Compare commits

...

147 Commits

Author SHA1 Message Date
Jacob Taylor b1bbf0f465 fix: Pin aws-lc-rs and aws-lc-sys to older version
works around build issue:
https://github.com/aws/aws-lc-rs/issues/1168
2026-07-04 20:53:12 -07:00
Ginger 3467dffd94 fix: Adjust sync watcher logic to be more explicit 2026-07-05 00:42:44 +00:00
timedout 3adeee04d7 chore: Fix all the bugs 2026-07-04 22:51:10 +01:00
Ginger 632d90921b fix: Fix circular service dependency 2026-07-04 17:04:01 -04:00
31a05b9c 6aab456466 fix: appservice masquerading requirement check 2026-07-04 18:21:51 +00:00
theS1LV3R 7fac885a98 Merge branch 'main' into rewrite-resolver 2026-07-04 19:55:28 +02:00
Ginger d2b795d8cf refactor: Logic cleanup 2026-07-04 13:15:54 -04:00
Ginger e0a754483a fix: Give invitees creator status when creating a trusted private v12 room 2026-07-04 13:06:49 -04:00
Renovate Bot b61f0af98a chore(deps): update rust crate bytesize to v2.4.2 2026-07-04 15:19:04 +00:00
Renovate Bot 649f16aa14 chore(deps): update node-patch-updates to v2.0.16 2026-07-04 15:18:58 +00:00
Renovate Bot 54b63327d5 chore(deps): update rust crate arrayvec to v0.7.8 2026-07-04 15:18:44 +00:00
theS1LV3R 58507aa99b chore: Fix dependencies, prek errors 2026-07-03 10:59:14 +02:00
theS1LV3R e58ad49d11 fix: Remove unused code, use builder when not using cache 2026-07-03 10:59:13 +02:00
theS1LV3R 44087d8c09 chore: Fix more merge issues and removed methods 2026-07-03 10:59:13 +02:00
theS1LV3R fb74da21d9 style: Cargo fmt 2026-07-03 10:59:13 +02:00
theS1LV3R 76aa54770d chore: Fix somehow broken cargo.toml 2026-07-03 10:59:13 +02:00
theS1LV3R f6d43a9cf3 chore: Update news fragment 2026-07-03 10:59:12 +02:00
theS1LV3R eb078ee6dd chore: Pass http client into resolver 2026-07-03 10:59:12 +02:00
theS1LV3R d8eb01bf74 fix: Apply prek fixes 2026-07-03 10:59:12 +02:00
theS1LV3R 5b5ba53980 fix: Fix merge problems, update to resolvematrix 0.1.0 2026-07-03 10:59:12 +02:00
theS1LV3R 0a12d40625 chore: clippy
diff --git c/src/admin/query/resolver.rs i/src/admin/query/resolver.rs
index add4c70de..37d6797ff 100644
--- c/src/admin/query/resolver.rs
+++ i/src/admin/query/resolver.rs
@@ -96,7 +96,10 @@ async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result
 		    self.services.resolver.dns.cache.clear().await;
 		    writeln!(self, "Resolver caches cleared!").await
 	    } else if let Some(name) = name {
-		    self.services.resolver.resolver.remove_cache_entry(&name.as_str());
+		    self.services
+                .resolver
+                .resolver
+                .remove_cache_entry(name.as_str());
 			self.services.resolver.dns.cache.del_destination(&name);
 			self.services.resolver.dns.cache.del_override(&name);
 			self.write_str(&format!("Cleared {name} from resolver caches!"))
2026-07-03 10:59:12 +02:00
theS1LV3R 3d521c8898 feat: Add cache clearing
diff --git c/Cargo.lock i/Cargo.lock
index 4a8b1e273..239932c62 100644
--- c/Cargo.lock
+++ i/Cargo.lock
@@ -4601,9 +4601,9 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"

 [[package]]
 name = "resolvematrix"
-version = "0.0.3"
+version = "0.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52dfcc6f56a81348db1fc6591356cfea9dc840408c75553b2fe225f86de43274"
+checksum = "ea2b9bc3cb1220455bf50519f315c27257859059cb1b0f1643758afd03cbef8a"
 dependencies = [
  "hickory-resolver",
  "reqwest 0.13.2",
diff --git c/Cargo.toml i/Cargo.toml
index f89a5e010..d6260177a 100644
--- c/Cargo.toml
+++ i/Cargo.toml
@@ -45,7 +45,7 @@ version = "1.0.6"
 version = "1.0.0"

 [workspace.dependencies.cargo_toml]
-version = "1.0"
+version = "0.22"
 default-features = false
 features = ["features"]

@@ -164,7 +164,7 @@ features = ["raw_value"]

 # Used for appservice registration files
 [workspace.dependencies.serde-saphyr]
-version = "0.0.28"
+version = "0.0.27"

 # Used to load forbidden room/user regex from config
 [workspace.dependencies.serde_regex]
@@ -356,7 +356,6 @@ features = [
     "ring-compat",
     "compat-upload-signatures",
     "compat-optional-txn-pdus",
-    "compat-get-3pids",
     "unstable-msc2666",
     "unstable-msc2867",
     "unstable-msc2870",
@@ -561,10 +560,7 @@ features = ["std"]
 version = "0.3.0"

 [workspace.dependencies.resolvematrix]
-version = "0.1.0"
-
-[workspace.dependencies.serde_urlencoded]
-version = "0.7.1"
+version = "0.0.4"

 #
 # Patches
diff --git c/src/admin/query/resolver.rs i/src/admin/query/resolver.rs
index 4a88650f8..add4c70de 100644
--- c/src/admin/query/resolver.rs
+++ i/src/admin/query/resolver.rs
@@ -92,9 +92,11 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {

 	async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
 		if all {
-			self.services.resolver.dns.cache.clear().await;
-			writeln!(self, "Resolver caches cleared!").await
-		} else if let Some(name) = name {
+		    self.services.resolver.resolver.clear_cache();
+		    self.services.resolver.dns.cache.clear().await;
+		    writeln!(self, "Resolver caches cleared!").await
+	    } else if let Some(name) = name {
+		    self.services.resolver.resolver.remove_cache_entry(&name.as_str());
 			self.services.resolver.dns.cache.del_destination(&name);
 			self.services.resolver.dns.cache.del_override(&name);
 			self.write_str(&format!("Cleared {name} from resolver caches!"))
diff --git c/src/service/resolver/mod.rs i/src/service/resolver/mod.rs
index c24b8ba60..6b57013dd 100644
--- c/src/service/resolver/mod.rs
+++ i/src/service/resolver/mod.rs
@@ -45,7 +45,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 	}

 	async fn clear_cache(&self) {
-		// No ability to clean resolvematrix cache at the moment
+		self.resolver.clear_cache();
 		self.dns.resolver.clear_cache();
 		self.dns.cache.clear().await;
 	}
2026-07-03 10:59:11 +02:00
theS1LV3R f8ad323a1e chore: Lockfile update
diff --git c/Cargo.lock i/Cargo.lock
index 9f1d6b35d..4a8b1e273 100644
--- c/Cargo.lock
+++ i/Cargo.lock
@@ -39,6 +39,24 @@ dependencies = [
  "memchr",
 ]

+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
 [[package]]
 name = "alloc-no-stdlib"
 version = "2.0.4"
@@ -47,9 +65,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"

 [[package]]
 name = "alloc-stdlib"
-version = "0.2.4"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
 dependencies = [
  "alloc-no-stdlib",
 ]
@@ -71,9 +89,9 @@ dependencies = [

 [[package]]
 name = "annotate-snippets"
-version = "0.12.16"
+version = "0.12.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1"
+checksum = "92570a3f9c98e7e84df84b71d0965ac99b1871fcd75a3773a3bd1bad13f64cf7"
 dependencies = [
  "anstyle",
  "memchr",
@@ -101,6 +119,12 @@ version = "1.0.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"

+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
 [[package]]
 name = "arc-swap"
 version = "1.9.1"
@@ -110,6 +134,17 @@ dependencies = [
  "rustversion",
 ]

+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "argon2"
 version = "0.5.3"
@@ -130,13 +165,22 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"

 [[package]]
 name = "arrayvec"
-version = "0.7.7"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 dependencies = [
  "serde",
 ]

+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
 [[package]]
 name = "as_variant"
 version = "1.3.0"
@@ -145,9 +189,9 @@ checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a"

 [[package]]
 name = "askama"
-version = "0.16.0"
+version = "0.15.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc"
+checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608"
 dependencies = [
  "askama_macros",
  "itoa",
@@ -158,13 +202,12 @@ dependencies = [

 [[package]]
 name = "askama_derive"
-version = "0.16.0"
+version = "0.15.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738"
+checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c"
 dependencies = [
  "askama_parser",
  "basic-toml",
- "glob",
  "memchr",
  "proc-macro2",
  "quote",
@@ -176,24 +219,63 @@ dependencies = [

 [[package]]
 name = "askama_macros"
-version = "0.16.0"
+version = "0.15.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a"
+checksum = "f0756b45480437dded0565dfc568af62ccce146fb6cfe902e808ba86e445f44f"
 dependencies = [
  "askama_derive",
 ]

 [[package]]
 name = "askama_parser"
-version = "0.16.0"
+version = "0.15.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da"
+checksum = "5d0af3691ba3af77949c0b5a3925444b85cb58a0184cc7fec16c68ba2e7be868"
 dependencies = [
  "rustc-hash",
  "serde",
  "serde_derive",
  "unicode-ident",
- "winnow 1.0.3",
+ "winnow 1.0.1",
+]
+
+[[package]]
+name = "asn1-rs"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]

 [[package]]
@@ -215,9 +297,9 @@ dependencies = [

 [[package]]
 name = "async-compression"
-version = "0.4.42"
+version = "0.4.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
 dependencies = [
  "compression-codecs",
  "compression-core",
@@ -253,15 +335,58 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"

 [[package]]
 name = "autocfg"
-version = "1.5.1"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.18",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom 8.0.0",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
+dependencies = [
+ "arrayvec",
+]

 [[package]]
 name = "aws-lc-rs"
-version = "1.17.0"
+version = "1.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
+checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
 dependencies = [
  "aws-lc-sys",
  "zeroize",
@@ -269,9 +394,9 @@ dependencies = [

 [[package]]
 name = "aws-lc-sys"
-version = "0.41.0"
+version = "0.39.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
+checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
 dependencies = [
  "cc",
  "cmake",
@@ -281,9 +406,9 @@ dependencies = [

 [[package]]
 name = "axum"
-version = "0.8.9"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
 dependencies = [
  "axum-core",
  "bytes",
@@ -314,12 +439,12 @@ dependencies = [

 [[package]]
 name = "axum-client-ip"
-version = "1.3.1"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8ba1af5b620232acf37f2eb6d22151ea465491e0b4c25f552d1990f64ec5a67"
+checksum = "dff8ee1869817523c8f91c20bf17fd932707f66c2e7e0b0f811b29a227289562"
 dependencies = [
  "axum",
- "client-ip",
+ "forwarded-header-value",
  "serde",
 ]

@@ -344,9 +469,9 @@ dependencies = [

 [[package]]
 name = "axum-extra"
-version = "0.12.6"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
+checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
 dependencies = [
  "axum",
  "axum-core",
@@ -367,13 +492,12 @@ dependencies = [

 [[package]]
 name = "axum-server"
-version = "0.8.0"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc"
+checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9"
 dependencies = [
  "arc-swap",
  "bytes",
- "either",
  "fs-err",
  "http",
  "http-body",
@@ -381,6 +505,7 @@ dependencies = [
  "hyper-util",
  "pin-project-lite",
  "rustls",
+ "rustls-pemfile",
  "rustls-pki-types",
  "tokio",
  "tokio-rustls",
@@ -389,8 +514,9 @@ dependencies = [

 [[package]]
 name = "axum-server-dual-protocol"
-version = "0.8.0"
-source = "git+https://github.com/vinchona/axum-server-dual-protocol.git?rev=ca6db055254255b74238673ce4135698e347d71c#ca6db055254255b74238673ce4135698e347d71c"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2164551db024e87f20316d164eab9f5ad342d8188b08051ceb15ca92a60ea7b7"
 dependencies = [
  "axum-server",
  "bytes",
@@ -461,15 +587,30 @@ dependencies = [
  "quote",
  "regex",
  "rustc-hash",
- "shlex 1.3.0",
+ "shlex",
  "syn",
 ]

 [[package]]
-name = "bitflags"
-version = "2.13.0"
+name = "bit_field"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "bitstream-io"
+version = "4.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
+dependencies = [
+ "core2",
+]

 [[package]]
 name = "blake2"
@@ -477,7 +618,7 @@ version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
 dependencies = [
- "digest 0.10.7",
+ "digest",
 ]

 [[package]]
@@ -489,15 +630,6 @@ dependencies = [
  "generic-array",
 ]

-[[package]]
-name = "block-buffer"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
-dependencies = [
- "hybrid-array",
-]
-
 [[package]]
 name = "block2"
 version = "0.6.2"
@@ -508,10 +640,19 @@ dependencies = [
 ]

 [[package]]
-name = "brotli"
-version = "8.0.4"
+name = "blurhash"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
+dependencies = [
+ "image",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -520,9 +661,9 @@ dependencies = [

 [[package]]
 name = "brotli-decompressor"
-version = "5.0.3"
+version = "5.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -530,15 +671,15 @@ dependencies = [

 [[package]]
 name = "built"
-version = "0.8.1"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"

 [[package]]
 name = "bumpalo"
-version = "3.20.3"
+version = "3.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"

 [[package]]
 name = "bytemuck"
@@ -560,15 +701,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"

 [[package]]
 name = "bytes"
-version = "1.12.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"

 [[package]]
 name = "bytesize"
-version = "2.4.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d"
+checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"

 [[package]]
 name = "bzip2-sys"
@@ -591,9 +732,9 @@ dependencies = [

 [[package]]
 name = "cargo-platform"
-version = "0.3.3"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba"
+checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082"
 dependencies = [
  "serde",
  "serde_core",
@@ -610,32 +751,37 @@ dependencies = [
  "semver",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
 ]

 [[package]]
 name = "cargo_toml"
-version = "1.0.0"
+version = "0.22.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa61aec073ec94791433ddf3df2323ff9d1711557c2a0eefb0f99cb4f8dca520"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
 dependencies = [
- "semver",
  "serde",
- "toml 1.1.2+spec-1.1.0",
+ "toml 0.9.12+spec-1.1.0",
 ]

 [[package]]
 name = "cc"
-version = "1.2.64"
+version = "1.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
+checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
  "libc",
- "shlex 2.0.1",
+ "shlex",
 ]

+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
 [[package]]
 name = "cexpr"
 version = "0.6.0"
@@ -665,7 +811,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.3.0",
- "rand_core 0.10.1",
+ "rand_core 0.10.0",
 ]

 [[package]]
@@ -679,9 +825,9 @@ dependencies = [

 [[package]]
 name = "chrono"
-version = "0.4.45"
+version = "0.4.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
 dependencies = [
  "num-traits",
 ]
@@ -699,9 +845,9 @@ dependencies = [

 [[package]]
 name = "clap"
-version = "4.6.1"
+version = "4.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -719,9 +865,9 @@ dependencies = [

 [[package]]
 name = "clap_derive"
-version = "4.6.1"
+version = "4.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -735,15 +881,6 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

-[[package]]
-name = "client-ip"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39d2056bf065c8b4bce5a8898d40e175211ff4410add2a84d695845d3937c729"
-dependencies = [
- "http",
-]
-
 [[package]]
 name = "cmake"
 version = "0.1.58"
@@ -753,12 +890,6 @@ dependencies = [
  "cc",
 ]

-[[package]]
-name = "cmov"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
-
 [[package]]
 name = "color_quant"
 version = "1.1.0"
@@ -777,9 +908,9 @@ dependencies = [

 [[package]]
 name = "compression-codecs"
-version = "0.4.38"
+version = "0.4.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
 dependencies = [
  "brotli",
  "compression-core",
@@ -791,9 +922,9 @@ dependencies = [

 [[package]]
 name = "compression-core"
-version = "0.4.32"
+version = "0.4.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"

 [[package]]
 name = "concurrent-queue"
@@ -806,9 +937,8 @@ dependencies = [

 [[package]]
 name = "conduwuit"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "aws-lc-rs",
  "clap",
  "conduwuit_admin",
  "conduwuit_api",
@@ -821,15 +951,12 @@ dependencies = [
  "console-subscriber",
  "const-str",
  "ctor",
- "dtor",
  "hardened_malloc-rs",
  "log",
  "opentelemetry",
  "opentelemetry-otlp",
  "opentelemetry_sdk",
  "parking_lot",
- "reqwest 0.13.4",
- "rustls",
  "sentry",
  "sentry-tower",
  "sentry-tracing",
@@ -844,9 +971,8 @@ dependencies = [

 [[package]]
 name = "conduwuit_admin"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "assign",
  "clap",
  "conduwuit_api",
  "conduwuit_core",
@@ -855,10 +981,10 @@ dependencies = [
  "conduwuit_service",
  "const-str",
  "ctor",
- "dtor",
  "futures",
  "lettre",
  "log",
+ "resolvematrix",
  "ruma",
  "serde-saphyr",
  "serde_json",
@@ -869,9 +995,8 @@ dependencies = [

 [[package]]
 name = "conduwuit_api"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "assign",
  "async-trait",
  "axum",
  "axum-client-ip",
@@ -883,31 +1008,29 @@ dependencies = [
  "conduwuit_service",
  "const-str",
  "ctor",
- "dtor",
  "futures",
  "hmac",
  "http",
  "http-body-util",
  "hyper",
  "ipaddress",
- "itertools 0.15.0",
+ "itertools 0.14.0",
  "lettre",
  "log",
  "rand 0.10.1",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
  "ruma",
- "ruminuwuity",
  "serde",
  "serde_html_form",
  "serde_json",
- "sha1 0.11.0",
+ "sha1",
  "tokio",
  "tracing",
 ]

 [[package]]
 name = "conduwuit_build_metadata"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "built",
  "cargo_metadata",
@@ -915,11 +1038,10 @@ dependencies = [

 [[package]]
 name = "conduwuit_core"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "argon2",
  "arrayvec",
- "assign",
  "axum",
  "axum-extra",
  "bytes",
@@ -934,7 +1056,6 @@ dependencies = [
  "core_affinity",
  "ctor",
  "cyborgtime",
- "dtor",
  "either",
  "figment",
  "futures",
@@ -943,36 +1064,37 @@ dependencies = [
  "http-body-util",
  "hyper-util",
  "ipaddress",
- "itertools 0.15.0",
+ "itertools 0.14.0",
  "lettre",
  "libc",
  "libloading 0.9.0",
  "lock_api",
  "log",
  "maplit",
- "nix",
+ "nix 0.31.2",
  "num-traits",
  "parking_lot",
  "rand 0.10.1",
  "rand_core 0.6.4",
  "regex",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
+ "resolvematrix",
+ "ring",
  "ruma",
  "sanitize-filename",
  "serde",
  "serde-saphyr",
  "serde_json",
  "serde_regex",
- "sha2 0.11.0",
  "smallstr",
  "smallvec",
- "thiserror",
+ "thiserror 2.0.18",
  "tikv-jemalloc-ctl",
  "tikv-jemalloc-sys",
  "tikv-jemallocator",
  "tokio",
  "tokio-metrics",
- "toml 1.1.2+spec-1.1.0",
+ "toml 0.9.12+spec-1.1.0",
  "tracing",
  "tracing-core",
  "tracing-subscriber",
@@ -981,14 +1103,13 @@ dependencies = [

 [[package]]
 name = "conduwuit_database"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "async-channel",
  "conduwuit_core",
  "conduwuit_macros",
  "const-str",
  "ctor",
- "dtor",
  "futures",
  "log",
  "minicbor",
@@ -1002,10 +1123,10 @@ dependencies = [

 [[package]]
 name = "conduwuit_macros"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "cargo_toml",
- "itertools 0.15.0",
+ "itertools 0.14.0",
  "proc-macro2",
  "quote",
  "syn",
@@ -1013,9 +1134,8 @@ dependencies = [

 [[package]]
 name = "conduwuit_router"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "assign",
  "axum",
  "axum-client-ip",
  "axum-server",
@@ -1029,7 +1149,6 @@ dependencies = [
  "conduwuit_web",
  "const-str",
  "ctor",
- "dtor",
  "futures",
  "http",
  "http-body-util",
@@ -1045,25 +1164,24 @@ dependencies = [
  "serde_json",
  "tokio",
  "tower",
- "tower-http 0.7.0",
+ "tower-http",
  "tracing",
 ]

 [[package]]
 name = "conduwuit_service"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "askama",
- "assign",
  "async-trait",
  "base64 0.22.1",
+ "blurhash",
  "bytes",
  "conduwuit_core",
  "conduwuit_database",
  "conduwuit_macros",
  "const-str",
  "ctor",
- "dtor",
  "either",
  "futures",
  "governor",
@@ -1071,7 +1189,8 @@ dependencies = [
  "http",
  "image",
  "ipaddress",
- "itertools 0.15.0",
+ "itertools 0.14.0",
+ "ldap3",
  "lettre",
  "log",
  "loole",
@@ -1081,16 +1200,14 @@ dependencies = [
  "recaptcha-verify",
  "regex",
  "reqwest 0.12.28",
- "reqwest 0.13.4",
+ "resolvematrix",
  "ruma",
- "ruminuwuity",
  "rustyline-async",
  "sd-notify",
  "serde",
  "serde-saphyr",
  "serde_json",
- "serde_urlencoded",
- "sha2 0.11.0",
+ "sha2",
  "termimad",
  "tokio",
  "tracing",
@@ -1101,37 +1218,25 @@ dependencies = [

 [[package]]
 name = "conduwuit_web"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "askama",
- "assign",
  "async-trait",
  "axum",
  "axum-extra",
  "base64 0.22.1",
- "conduwuit_api",
  "conduwuit_build_metadata",
  "conduwuit_core",
- "conduwuit_database",
  "conduwuit_service",
- "form_urlencoded",
  "futures",
- "lettre",
  "memory-serve",
  "rand 0.10.1",
- "recaptcha-verify",
- "reqwest 0.12.28",
  "ruma",
  "serde",
- "serde_json",
- "serde_urlencoded",
- "thiserror",
- "tower-http 0.7.0",
+ "thiserror 2.0.18",
+ "tower-http",
  "tower-sec-fetch",
- "tower-sessions",
- "tower-sessions-core",
  "tracing",
- "url",
  "validator",
 ]

@@ -1183,9 +1288,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"

 [[package]]
 name = "const-str"
-version = "1.1.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
+checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160"

 [[package]]
 name = "const_panic"
@@ -1196,6 +1301,16 @@ dependencies = [
  "typewit",
 ]

+[[package]]
+name = "continuwuity-admin-api"
+version = "0.1.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
+dependencies = [
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "convert_case"
 version = "0.10.0"
@@ -1225,16 +1340,6 @@ dependencies = [
  "crossterm",
 ]

-[[package]]
-name = "core-foundation"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
 [[package]]
 name = "core-foundation"
 version = "0.10.1"
@@ -1251,6 +1356,15 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"

+[[package]]
+name = "core2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "core_affinity"
 version = "0.8.3"
@@ -1404,6 +1518,12 @@ dependencies = [
  "winapi",
 ]

+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
 [[package]]
 name = "crypto-common"
 version = "0.1.7"
@@ -1414,33 +1534,21 @@ dependencies = [
  "typenum",
 ]

-[[package]]
-name = "crypto-common"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
-dependencies = [
- "hybrid-array",
-]
-
 [[package]]
 name = "ctor"
-version = "1.0.7"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a"
+checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
 dependencies = [
- "link-section",
- "linktime-proc-macro",
+ "ctor-proc-macro",
+ "dtor",
 ]

 [[package]]
-name = "ctutils"
-version = "0.4.2"
+name = "ctor-proc-macro"
+version = "0.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
-dependencies = [
- "cmov",
-]
+checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"

 [[package]]
 name = "curve25519-dalek"
@@ -1451,7 +1559,7 @@ dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
  "curve25519-dalek-derive",
- "digest 0.10.7",
+ "digest",
  "fiat-crypto",
  "rustc_version",
  "subtle",
@@ -1512,9 +1620,9 @@ dependencies = [

 [[package]]
 name = "data-encoding"
-version = "2.11.0"
+version = "2.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"

 [[package]]
 name = "date_header"
@@ -1542,13 +1650,27 @@ dependencies = [
  "zeroize",
 ]

+[[package]]
+name = "der-parser"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
 [[package]]
 name = "deranged"
 version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
 dependencies = [
- "serde_core",
+ "powerfmt",
 ]

 [[package]]
@@ -1579,22 +1701,11 @@ version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
- "block-buffer 0.10.4",
- "crypto-common 0.1.7",
+ "block-buffer",
+ "crypto-common",
  "subtle",
 ]

-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer 0.12.1",
- "crypto-common 0.2.2",
- "ctutils",
-]
-
 [[package]]
 name = "dispatch2"
 version = "0.3.1"
@@ -1607,9 +1718,9 @@ dependencies = [

 [[package]]
 name = "displaydoc"
-version = "0.2.6"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1626,14 +1737,30 @@ dependencies = [
 ]

 [[package]]
-name = "dtor"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d738e43aa64edab57c983d56de890d65fea7dc05605490c74451ce721dfd84b"
+name = "draupnir-antispam"
+version = "0.1.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
- "linktime-proc-macro",
+ "ruma-common",
+ "serde",
+ "serde_json",
 ]

+[[package]]
+name = "dtor"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301"
+dependencies = [
+ "dtor-proc-macro",
+]
+
+[[package]]
+name = "dtor-proc-macro"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
 [[package]]
 name = "dunce"
 version = "1.0.5"
@@ -1658,17 +1785,18 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
+ "rand_core 0.6.4",
  "serde",
- "sha2 0.10.9",
+ "sha2",
  "subtle",
  "zeroize",
 ]

 [[package]]
 name = "either"
-version = "1.16.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 dependencies = [
  "serde",
 ]
@@ -1707,6 +1835,38 @@ dependencies = [
  "encoding_rs",
 ]

+[[package]]
+name = "enum-as-inner"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.2"
@@ -1742,12 +1902,47 @@ dependencies = [
  "pin-project-lite",
 ]

+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"

+[[package]]
+name = "fax"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
+dependencies = [
+ "fax_derive",
+]
+
+[[package]]
+name = "fax_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "fdeflate"
 version = "0.3.7"
@@ -1838,6 +2033,16 @@ dependencies = [
  "percent-encoding",
 ]

+[[package]]
+name = "forwarded-header-value"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
+dependencies = [
+ "nonempty",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "fs-err"
 version = "3.3.0"
@@ -1937,9 +2142,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"

 [[package]]
 name = "futures-timer"
-version = "3.0.4"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"

 [[package]]
 name = "futures-util"
@@ -2004,7 +2209,7 @@ dependencies = [
  "cfg-if",
  "libc",
  "r-efi 6.0.0",
- "rand_core 0.10.1",
+ "rand_core 0.10.0",
  "wasip2",
  "wasip3",
 ]
@@ -2050,21 +2255,11 @@ dependencies = [
  "web-time",
 ]

-[[package]]
-name = "granit-parser"
-version = "0.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14668345710c92cb04181d9268407067689327d4b055273e3c4179a0777ee6a1"
-dependencies = [
- "arraydeque",
- "smallvec",
-]
-
 [[package]]
 name = "h2"
-version = "0.4.15"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2107,6 +2302,17 @@ dependencies = [
  "tokio-util",
 ]

+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
 [[package]]
 name = "hardened_malloc-rs"
 version = "0.1.2+12"
@@ -2135,9 +2341,9 @@ dependencies = [

 [[package]]
 name = "hashbrown"
-version = "0.17.1"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"

 [[package]]
 name = "hdrhistogram"
@@ -2164,7 +2370,7 @@ dependencies = [
  "http",
  "httpdate",
  "mime",
- "sha1 0.10.6",
+ "sha1",
 ]

 [[package]]
@@ -2195,84 +2401,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"

 [[package]]
-name = "hickory-net"
-version = "0.26.1"
+name = "hickory-proto"
+version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
+checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
 dependencies = [
  "async-trait",
  "cfg-if",
  "data-encoding",
+ "enum-as-inner",
  "futures-channel",
  "futures-io",
  "futures-util",
- "hickory-proto",
  "idna",
  "ipnet",
- "jni",
- "rand 0.10.1",
- "thiserror",
- "tinyvec",
- "tokio",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "hickory-proto"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
-dependencies = [
- "data-encoding",
- "idna",
- "ipnet",
- "jni",
  "once_cell",
- "prefix-trie",
- "rand 0.10.1",
+ "rand 0.9.3",
  "ring",
  "serde",
- "thiserror",
+ "thiserror 2.0.18",
  "tinyvec",
+ "tokio",
  "tracing",
  "url",
 ]

 [[package]]
 name = "hickory-resolver"
-version = "0.26.1"
+version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
+checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
 dependencies = [
  "cfg-if",
  "futures-util",
- "hickory-net",
  "hickory-proto",
  "ipconfig",
- "ipnet",
- "jni",
  "moka",
- "ndk-context",
  "once_cell",
  "parking_lot",
- "rand 0.10.1",
+ "rand 0.9.3",
  "resolv-conf",
  "serde",
  "smallvec",
- "system-configuration",
- "thiserror",
+ "thiserror 2.0.18",
  "tokio",
  "tracing",
 ]

 [[package]]
 name = "hmac"
-version = "0.13.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
- "digest 0.11.3",
+ "digest",
 ]

 [[package]]
@@ -2302,9 +2484,9 @@ dependencies = [

 [[package]]
 name = "http"
-version = "1.4.2"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
 dependencies = [
  "bytes",
  "itoa",
@@ -2360,20 +2542,11 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"

-[[package]]
-name = "hybrid-array"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
-dependencies = [
- "typenum",
-]
-
 [[package]]
 name = "hyper"
-version = "1.10.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2393,18 +2566,20 @@ dependencies = [

 [[package]]
 name = "hyper-rustls"
-version = "0.27.9"
+version = "0.27.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
 dependencies = [
  "http",
  "hyper",
  "hyper-util",
  "rustls",
  "rustls-native-certs",
+ "rustls-pki-types",
  "tokio",
  "tokio-rustls",
  "tower-service",
+ "webpki-roots",
 ]

 [[package]]
@@ -2549,9 +2724,9 @@ dependencies = [

 [[package]]
 name = "idna_adapter"
-version = "1.2.2"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
 dependencies = [
  "icu_normalizer",
  "icu_properties",
@@ -2566,11 +2741,17 @@ dependencies = [
  "bytemuck",
  "byteorder-lite",
  "color_quant",
+ "exr",
  "gif",
  "image-webp",
  "moxcms",
  "num-traits",
  "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
  "zune-core",
  "zune-jpeg",
 ]
@@ -2585,6 +2766,12 @@ dependencies = [
  "quick-error",
 ]

+[[package]]
+name = "imgref"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
+
 [[package]]
 name = "indexmap"
 version = "2.14.0"
@@ -2592,7 +2779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
 dependencies = [
  "equivalent",
- "hashbrown 0.17.1",
+ "hashbrown 0.17.0",
  "serde",
  "serde_core",
 ]
@@ -2603,6 +2790,17 @@ version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"

+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "ipaddress"
 version = "0.1.3"
@@ -2635,7 +2833,14 @@ name = "ipnet"
 version = "2.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
 dependencies = [
+ "memchr",
  "serde",
 ]

@@ -2657,15 +2862,6 @@ dependencies = [
  "either",
 ]

-[[package]]
-name = "itertools"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itoa"
 version = "1.0.18"
@@ -2674,32 +2870,27 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"

 [[package]]
 name = "jni"
-version = "0.22.4"
+version = "0.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
 dependencies = [
+ "cesu8",
  "cfg-if",
  "combine",
- "jni-macros",
- "jni-sys",
+ "jni-sys 0.3.1",
  "log",
- "simd_cesu8",
- "thiserror",
+ "thiserror 1.0.69",
  "walkdir",
- "windows-link",
+ "windows-sys 0.45.0",
 ]

 [[package]]
-name = "jni-macros"
-version = "0.22.4"
+name = "jni-sys"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
 dependencies = [
- "proc-macro2",
- "quote",
- "rustc_version",
- "simd_cesu8",
- "syn",
+ "jni-sys 0.4.1",
 ]

 [[package]]
@@ -2733,12 +2924,13 @@ dependencies = [

 [[package]]
 name = "js-sys"
-version = "0.3.102"
+version = "0.3.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
 dependencies = [
  "cfg-if",
  "futures-util",
+ "once_cell",
  "wasm-bindgen",
 ]

@@ -2753,20 +2945,30 @@ dependencies = [

 [[package]]
 name = "js_option"
-version = "0.2.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c"
+checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c"
 dependencies = [
- "serde_core",
+ "serde",
 ]

 [[package]]
 name = "konst"
-version = "0.4.3"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d"
+checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9"
 dependencies = [
  "const_panic",
+ "konst_kernel",
+ "typewit",
+]
+
+[[package]]
+name = "konst_kernel"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c"
+dependencies = [
  "typewit",
 ]

@@ -2799,6 +3001,41 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"

+[[package]]
+name = "lber"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbcf559624bfd9fe8d488329a8959766335a43a9b8b2cdd6a2c379fca02909a5"
+dependencies = [
+ "bytes",
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "ldap3"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01fe89f5e7cfb7e4701e3a38ff9f00358e026a9aee940355d88ee9d81e5c7503"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures",
+ "futures-util",
+ "lber",
+ "log",
+ "nom 7.1.3",
+ "percent-encoding",
+ "rustls",
+ "rustls-native-certs",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tokio-util",
+ "url",
+ "x509-parser",
+]
+
 [[package]]
 name = "leb128fmt"
 version = "0.1.0"
@@ -2806,10 +3043,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"

 [[package]]
-name = "lettre"
-version = "0.11.22"
+name = "lebe"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
+[[package]]
+name = "lettre"
+version = "0.11.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
@@ -2837,9 +3080,19 @@ dependencies = [

 [[package]]
 name = "libc"
-version = "0.2.186"
+version = "0.2.184"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
+dependencies = [
+ "arbitrary",
+ "cc",
+]

 [[package]]
 name = "libloading"
@@ -2863,33 +3116,21 @@ dependencies = [

 [[package]]
 name = "libz-sys"
-version = "1.1.29"
+version = "1.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
+checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
 dependencies = [
  "cc",
  "pkg-config",
  "vcpkg",
 ]

-[[package]]
-name = "link-section"
-version = "0.18.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2b1dd6fe32e55c0fc0ea9493aa57459ca3cf4ff3c857c7d0302290150da6e4f"
-
 [[package]]
 name = "linked-hash-map"
 version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"

-[[package]]
-name = "linktime-proc-macro"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee"
-
 [[package]]
 name = "linux-raw-sys"
 version = "0.12.1"
@@ -2919,9 +3160,9 @@ dependencies = [

 [[package]]
 name = "log"
-version = "0.4.33"
+version = "0.4.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"

 [[package]]
 name = "loole"
@@ -2933,6 +3174,15 @@ dependencies = [
  "futures-sink",
 ]

+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
 [[package]]
 name = "lru-cache"
 version = "0.1.2"
@@ -3012,10 +3262,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"

 [[package]]
-name = "memchr"
-version = "2.8.2"
+name = "maybe-rayon"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"

 [[package]]
 name = "memory-serve"
@@ -3033,6 +3293,16 @@ dependencies = [
  "walkdir",
 ]

+[[package]]
+name = "meowlnir-antispam"
+version = "0.1.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
+dependencies = [
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "mime"
 version = "0.3.17"
@@ -3051,15 +3321,15 @@ dependencies = [

 [[package]]
 name = "minicbor"
-version = "2.2.2"
+version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b7a5041e12946f8b7d3f5a9d96383a19d694b9335457c522be7815b9abafb02"
+checksum = "e70eae6d4f18f7d76877fe7b13f0bc21f7c2b7239d2041c338335f7b388d0dd7"

 [[package]]
 name = "minicbor-serde"
-version = "0.7.0"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "293c7245401f035e2dcc4b12ebdb5c9d8847247fc79fe1b5b0a0d58d7275324c"
+checksum = "80047f75e28e3b38f6ab2ec3c2c7669f6b411fa6f8424e1a90a3fd784b19a3f4"
 dependencies = [
  "minicbor",
  "serde",
@@ -3092,9 +3362,9 @@ dependencies = [

 [[package]]
 name = "mio"
-version = "1.2.1"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
 dependencies = [
  "libc",
  "log",
@@ -3129,12 +3399,6 @@ dependencies = [
  "pxfm",
 ]

-[[package]]
-name = "ndk-context"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
-
 [[package]]
 name = "new_debug_unreachable"
 version = "1.0.6"
@@ -3143,9 +3407,21 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"

 [[package]]
 name = "nix"
-version = "0.31.3"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nix"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
 dependencies = [
  "bitflags",
  "cfg-if",
@@ -3178,12 +3454,24 @@ dependencies = [
  "memchr",
 ]

+[[package]]
+name = "nonempty"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
+
 [[package]]
 name = "nonzero_ext"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"

+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
 [[package]]
 name = "nu-ansi-term"
 version = "0.50.3"
@@ -3228,9 +3516,20 @@ dependencies = [

 [[package]]
 name = "num-conv"
-version = "0.2.2"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]

 [[package]]
 name = "num-integer"
@@ -3450,6 +3749,15 @@ dependencies = [
  "memchr",
 ]

+[[package]]
+name = "oid-registry"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
+dependencies = [
+ "asn1-rs",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.21.4"
@@ -3468,36 +3776,36 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"

 [[package]]
 name = "opentelemetry"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682"
+checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0"
 dependencies = [
  "futures-core",
  "futures-sink",
  "js-sys",
  "pin-project-lite",
- "thiserror",
+ "thiserror 2.0.18",
  "tracing",
 ]

 [[package]]
 name = "opentelemetry-http"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331"
+checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d"
 dependencies = [
  "async-trait",
  "bytes",
  "http",
  "opentelemetry",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
 ]

 [[package]]
 name = "opentelemetry-otlp"
-version = "0.32.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35"
+checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f"
 dependencies = [
  "http",
  "opentelemetry",
@@ -3505,18 +3813,18 @@ dependencies = [
  "opentelemetry-proto",
  "opentelemetry_sdk",
  "prost",
- "reqwest 0.13.4",
- "thiserror",
+ "reqwest 0.12.28",
+ "thiserror 2.0.18",
  "tokio",
  "tonic",
- "tonic-types",
+ "tracing",
 ]

 [[package]]
 name = "opentelemetry-proto"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638"
+checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
 dependencies = [
  "opentelemetry",
  "opentelemetry_sdk",
@@ -3527,31 +3835,30 @@ dependencies = [

 [[package]]
 name = "opentelemetry_sdk"
-version = "0.32.1"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9"
+checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd"
 dependencies = [
  "futures-channel",
  "futures-executor",
  "futures-util",
  "opentelemetry",
  "percent-encoding",
- "portable-atomic",
- "rand 0.9.4",
- "thiserror",
+ "rand 0.9.3",
+ "thiserror 2.0.18",
  "tokio",
  "tokio-stream",
 ]

 [[package]]
 name = "os_info"
-version = "3.15.0"
+version = "3.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b"
+checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
 dependencies = [
  "android_system_properties",
  "log",
- "nix",
+ "nix 0.30.1",
  "objc2",
  "objc2-foundation",
  "objc2-ui-kit",
@@ -3607,6 +3914,12 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"

+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
 [[package]]
 name = "pear"
 version = "0.2.9"
@@ -3672,7 +3985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
 dependencies = [
  "phf_shared",
- "rand 0.8.6",
+ "rand 0.8.5",
 ]

 [[package]]
@@ -3686,18 +3999,18 @@ dependencies = [

 [[package]]
 name = "pin-project"
-version = "1.1.13"
+version = "1.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
 dependencies = [
  "pin-project-internal",
 ]

 [[package]]
 name = "pin-project-internal"
-version = "1.1.13"
+version = "1.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3722,9 +4035,9 @@ dependencies = [

 [[package]]
 name = "pkg-config"
-version = "0.3.33"
+version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"

 [[package]]
 name = "png"
@@ -3775,17 +4088,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"

-[[package]]
-name = "prefix-trie"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
-dependencies = [
- "either",
- "ipnet",
- "num-traits",
-]
-
 [[package]]
 name = "prettyplease"
 version = "0.2.37"
@@ -3802,7 +4104,7 @@ version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
 dependencies = [
- "toml_edit 0.25.12+spec-1.1.0",
+ "toml_edit 0.25.11+spec-1.1.0",
 ]

 [[package]]
@@ -3850,10 +4152,29 @@ dependencies = [
 ]

 [[package]]
-name = "prost"
-version = "0.14.4"
+name = "profiling"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -3861,9 +4182,9 @@ dependencies = [

 [[package]]
 name = "prost-derive"
-version = "0.14.4"
+version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
+checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
 dependencies = [
  "anyhow",
  "itertools 0.14.0",
@@ -3874,18 +4195,18 @@ dependencies = [

 [[package]]
 name = "prost-types"
-version = "0.14.4"
+version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
+checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
 dependencies = [
  "prost",
 ]

 [[package]]
 name = "pulldown-cmark"
-version = "0.13.4"
+version = "0.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
+checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad"
 dependencies = [
  "bitflags",
  "memchr",
@@ -3901,9 +4222,18 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"

 [[package]]
 name = "pxfm"
-version = "0.1.29"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]

 [[package]]
 name = "quick-error"
@@ -3926,7 +4256,7 @@ dependencies = [
  "rustc-hash",
  "rustls",
  "socket2",
- "thiserror",
+ "thiserror 2.0.18",
  "tokio",
  "tracing",
  "web-time",
@@ -3942,13 +4272,13 @@ dependencies = [
  "bytes",
  "getrandom 0.3.4",
  "lru-slab",
- "rand 0.9.4",
+ "rand 0.9.3",
  "ring",
  "rustc-hash",
  "rustls",
  "rustls-pki-types",
  "slab",
- "thiserror",
+ "thiserror 2.0.18",
  "tinyvec",
  "tracing",
  "web-time",
@@ -3970,9 +4300,9 @@ dependencies = [

 [[package]]
 name = "quote"
-version = "1.0.46"
+version = "1.0.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
 dependencies = [
  "proc-macro2",
 ]
@@ -3997,18 +4327,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"

 [[package]]
 name = "rand"
-version = "0.8.6"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "rand_core 0.6.4",
 ]

 [[package]]
 name = "rand"
-version = "0.9.4"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
 dependencies = [
  "rand_chacha",
  "rand_core 0.9.5",
@@ -4022,7 +4352,7 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
 dependencies = [
  "chacha20",
  "getrandom 0.4.2",
- "rand_core 0.10.1",
+ "rand_core 0.10.0",
 ]

 [[package]]
@@ -4055,9 +4385,79 @@ dependencies = [

 [[package]]
 name = "rand_core"
-version = "0.10.1"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
+
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools 0.14.0",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.3",
+ "rand_chacha",
+ "simd_helpers",
+ "thiserror 2.0.18",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]

 [[package]]
 name = "recaptcha-verify"
@@ -4081,9 +4481,9 @@ dependencies = [

 [[package]]
 name = "regex"
-version = "1.12.4"
+version = "1.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -4104,52 +4504,15 @@ dependencies = [

 [[package]]
 name = "regex-syntax"
-version = "0.8.11"
+version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"

 [[package]]
 name = "reqwest"
 version = "0.12.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-rustls",
- "hyper-util",
- "js-sys",
- "log",
- "percent-encoding",
- "pin-project-lite",
- "rustls",
- "rustls-native-certs",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper",
- "tokio",
- "tokio-rustls",
- "tower",
- "tower-http 0.6.11",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "reqwest"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -4173,6 +4536,47 @@ dependencies = [
  "pin-project-lite",
  "quinn",
  "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
  "rustls-pki-types",
  "rustls-platform-verifier",
  "serde",
@@ -4180,14 +4584,12 @@ dependencies = [
  "sync_wrapper",
  "tokio",
  "tokio-rustls",
- "tokio-util",
  "tower",
- "tower-http 0.6.11",
+ "tower-http",
  "tower-service",
  "url",
  "wasm-bindgen",
  "wasm-bindgen-futures",
- "wasm-streams",
  "web-sys",
 ]

@@ -4197,6 +4599,25 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"

+[[package]]
+name = "resolvematrix"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfcc6f56a81348db1fc6591356cfea9dc840408c75553b2fe225f86de43274"
+dependencies = [
+ "hickory-resolver",
+ "reqwest 0.13.2",
+ "serde",
+ "thiserror 2.0.18",
+ "tracing",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+
 [[package]]
 name = "ring"
 version = "0.17.14"
@@ -4213,27 +4634,31 @@ dependencies = [

 [[package]]
 name = "ruma"
-version = "0.15.1"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.10.1"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "assign",
+ "continuwuity-admin-api",
+ "draupnir-antispam",
  "js_int",
  "js_option",
+ "meowlnir-antispam",
  "ruma-appservice-api",
  "ruma-client-api",
  "ruma-common",
  "ruma-events",
  "ruma-federation-api",
+ "ruma-identifiers-validation",
+ "ruma-identity-service-api",
  "ruma-push-gateway-api",
  "ruma-signatures",
- "ruma-state-res",
  "web-time",
 ]

 [[package]]
 name = "ruma-appservice-api"
-version = "0.15.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.10.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "js_int",
  "ruma-common",
@@ -4244,12 +4669,13 @@ dependencies = [

 [[package]]
 name = "ruma-client-api"
-version = "0.23.1"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.18.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "as_variant",
  "assign",
  "bytes",
+ "date_header",
  "http",
  "js_int",
  "js_option",
@@ -4259,22 +4685,21 @@ dependencies = [
  "serde",
  "serde_html_form",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "url",
  "web-time",
 ]

 [[package]]
 name = "ruma-common"
-version = "0.18.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.13.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "as_variant",
  "base64 0.22.1",
  "bytes",
- "date_header",
  "form_urlencoded",
- "getrandom 0.4.2",
+ "getrandom 0.2.17",
  "http",
  "indexmap",
  "js_int",
@@ -4287,41 +4712,45 @@ dependencies = [
  "serde",
  "serde_html_form",
  "serde_json",
- "thiserror",
+ "smallvec",
+ "thiserror 2.0.18",
  "time",
  "tracing",
  "url",
  "uuid",
  "web-time",
  "wildmatch",
- "zeroize",
 ]

 [[package]]
 name = "ruma-events"
-version = "0.33.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.28.1"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "as_variant",
  "indexmap",
  "js_int",
  "js_option",
+ "percent-encoding",
  "pulldown-cmark",
+ "regex",
  "ruma-common",
+ "ruma-identifiers-validation",
  "ruma-macros",
  "serde",
  "serde_json",
- "thiserror",
+ "smallvec",
+ "thiserror 2.0.18",
  "tracing",
+ "url",
  "web-time",
  "wildmatch",
- "zeroize",
 ]

 [[package]]
 name = "ruma-federation-api"
-version = "0.14.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.9.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "bytes",
  "headers",
@@ -4334,28 +4763,36 @@ dependencies = [
  "rand 0.10.1",
  "ruma-common",
  "ruma-events",
- "ruma-signatures",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "tracing",
 ]

 [[package]]
 name = "ruma-identifiers-validation"
-version = "0.12.1"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.9.5"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "js_int",
- "thiserror",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ruma-identity-service-api"
+version = "0.9.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
+dependencies = [
+ "js_int",
+ "ruma-common",
+ "serde",
 ]

 [[package]]
 name = "ruma-macros"
-version = "0.18.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.13.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
- "as_variant",
  "cfg-if",
  "proc-macro-crate",
  "proc-macro2",
@@ -4363,13 +4800,13 @@ dependencies = [
  "ruma-identifiers-validation",
  "serde",
  "syn",
- "toml 1.1.2+spec-1.1.0",
+ "toml 0.8.23",
 ]

 [[package]]
 name = "ruma-push-gateway-api"
-version = "0.14.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.9.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "js_int",
  "ruma-common",
@@ -4380,50 +4817,25 @@ dependencies = [

 [[package]]
 name = "ruma-signatures"
-version = "0.20.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.15.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "base64 0.22.1",
  "ed25519-dalek",
- "memchr",
  "pkcs8",
  "rand 0.10.1",
+ "rand_core 0.6.4",
  "ruma-common",
  "serde_json",
- "sha2 0.10.9",
- "thiserror",
-]
-
-[[package]]
-name = "ruma-state-res"
-version = "0.16.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
-dependencies = [
- "js_int",
- "ruma-common",
- "ruma-events",
- "ruma-signatures",
- "serde",
- "serde_json",
- "thiserror",
- "tracing",
-]
-
-[[package]]
-name = "ruminuwuity"
-version = "26.6.0-alpha.1"
-dependencies = [
- "assign",
- "ruma",
- "serde",
- "serde_json",
- "wildmatch",
+ "sha2",
+ "subslice",
+ "thiserror 2.0.18",
 ]

 [[package]]
 name = "rust-librocksdb-sys"
-version = "0.45.1+11.1.1"
-source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=0a25ff92f7c09b55eec496b9c192c7d5136ab2b8#0a25ff92f7c09b55eec496b9c192c7d5136ab2b8"
+version = "0.42.0+10.10.1"
+source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9#31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
 dependencies = [
  "bindgen",
  "bzip2-sys",
@@ -4439,8 +4851,8 @@ dependencies = [

 [[package]]
 name = "rust-rocksdb"
-version = "0.49.1"
-source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=0a25ff92f7c09b55eec496b9c192c7d5136ab2b8#0a25ff92f7c09b55eec496b9c192c7d5136ab2b8"
+version = "0.46.0"
+source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9#31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
 dependencies = [
  "libc",
  "parking_lot",
@@ -4468,6 +4880,15 @@ dependencies = [
  "semver",
 ]

+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom 7.1.3",
+]
+
 [[package]]
 name = "rustix"
 version = "1.1.4"
@@ -4483,9 +4904,9 @@ dependencies = [

 [[package]]
 name = "rustls"
-version = "0.23.41"
+version = "0.23.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
 dependencies = [
  "aws-lc-rs",
  "log",
@@ -4499,9 +4920,9 @@ dependencies = [

 [[package]]
 name = "rustls-native-certs"
-version = "0.8.4"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
 dependencies = [
  "openssl-probe",
  "rustls-pki-types",
@@ -4510,10 +4931,19 @@ dependencies = [
 ]

 [[package]]
-name = "rustls-pki-types"
-version = "1.14.1"
+name = "rustls-pemfile"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
 dependencies = [
  "web-time",
  "zeroize",
@@ -4521,11 +4951,11 @@ dependencies = [

 [[package]]
 name = "rustls-platform-verifier"
-version = "0.7.0"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
 dependencies = [
- "core-foundation 0.10.1",
+ "core-foundation",
  "core-foundation-sys",
  "jni",
  "log",
@@ -4548,9 +4978,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"

 [[package]]
 name = "rustls-webpki"
-version = "0.103.13"
+version = "0.103.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4"
 dependencies = [
  "aws-lc-rs",
  "ring",
@@ -4574,7 +5004,7 @@ dependencies = [
  "futures-util",
  "pin-project",
  "thingbuf",
- "thiserror",
+ "thiserror 2.0.18",
  "unicode-segmentation",
 ]

@@ -4602,6 +5032,17 @@ dependencies = [
  "regex",
 ]

+[[package]]
+name = "saphyr-parser-bw"
+version = "0.0.611"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dec0c833db75dc98957956b303fe447ffc5eb13f2325ef4c2350f7f3aa69e3"
+dependencies = [
+ "arraydeque",
+ "smallvec",
+ "thiserror 2.0.18",
+]
+
 [[package]]
 name = "schannel"
 version = "0.1.29"
@@ -4619,9 +5060,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"

 [[package]]
 name = "sd-notify"
-version = "0.5.0"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4ef7359e694bfaf1dd27a30f9d760b54c00dfae9f19bd0c05a39bc9128fe76"
+checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4"
 dependencies = [
  "libc",
 ]
@@ -4633,7 +5074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
 dependencies = [
  "bitflags",
- "core-foundation 0.10.1",
+ "core-foundation",
  "core-foundation-sys",
  "libc",
  "security-framework-sys",
@@ -4661,13 +5102,14 @@ dependencies = [

 [[package]]
 name = "sentry"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1975064d9f3d9d87a7c8f0371f7fac10d876906ca3e39faad72e61c263ff9b87"
+checksum = "d92d893ba7469d361a6958522fa440e4e2bc8bf4c5803cd1bf40b9af63f8f9a8"
 dependencies = [
  "cfg_aliases",
  "httpdate",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
+ "rustls",
  "sentry-backtrace",
  "sentry-contexts",
  "sentry-core",
@@ -4677,13 +5119,14 @@ dependencies = [
  "sentry-tower",
  "sentry-tracing",
  "tokio",
+ "ureq",
 ]

 [[package]]
 name = "sentry-backtrace"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "134f552b6b147f77aeee46ece875d67a8bfc1d56fe3860d78446ed4c25bb5f9f"
+checksum = "5f8784d0a27b5cd4b5f75769ffc84f0b7580e3c35e1af9cd83cb90b612d769cc"
 dependencies = [
  "backtrace",
  "regex",
@@ -4692,9 +5135,9 @@ dependencies = [

 [[package]]
 name = "sentry-contexts"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98482b938fb1f31ecd23506fb7f08b2ba20d60b9cc72581b1205a9acd31d32fa"
+checksum = "0e5eb42f4cd4f9fdfec9e3b07b25a4c9769df83d218a7e846658984d5948ad3e"
 dependencies = [
  "hostname",
  "libc",
@@ -4706,11 +5149,11 @@ dependencies = [

 [[package]]
 name = "sentry-core"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cff96247f4dc36867511d108709e703eb354143e7893319d763d2dd1edb4f48"
+checksum = "b0b1e7ca40f965db239da279bf278d87b7407469b98835f27f0c8e59ed189b06"
 dependencies = [
- "rand 0.9.4",
+ "rand 0.9.3",
  "sentry-types",
  "serde",
  "serde_json",
@@ -4719,9 +5162,9 @@ dependencies = [

 [[package]]
 name = "sentry-debug-images"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b603a51b083141062a142d73e36391b14244b4bd0bfe28bcd80e8327bb1d7698"
+checksum = "002561e49ea3a9de316e2efadc40fae553921b8ff41448f02ea85fd135a778d6"
 dependencies = [
  "findshlibs",
  "sentry-core",
@@ -4729,9 +5172,9 @@ dependencies = [

 [[package]]
 name = "sentry-log"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74df4d09a38fd59da258269ffd7f344bd308470117d68eda5a95b4fbd65683d0"
+checksum = "e200860daf76e09f9ad111bce25928f96bedbb84bc5934b37f05bb445727c70e"
 dependencies = [
  "bitflags",
  "log",
@@ -4740,9 +5183,9 @@ dependencies = [

 [[package]]
 name = "sentry-panic"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61de3f4a1fc77d4c57e8bacd8ef064c26aaa88ea5b2541a761d37c7c3703b9a9"
+checksum = "8906f8be87aea5ac7ef937323fb655d66607427f61007b99b7cb3504dc5a156c"
 dependencies = [
  "sentry-backtrace",
  "sentry-core",
@@ -4750,9 +5193,9 @@ dependencies = [

 [[package]]
 name = "sentry-tower"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d5cb61301e328cbd42d2fede094aca746674b359fcd9c44691f027820e8fe59"
+checksum = "56aebe376310840b49dad4cca55c7b32d9abdc14946cd071d4158ecb149b63a4"
 dependencies = [
  "http",
  "pin-project",
@@ -4764,9 +5207,9 @@ dependencies = [

 [[package]]
 name = "sentry-tracing"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc59b27dae3bb495e37e6d62d252214840a2e7e1875531ee9fa2953df8985537"
+checksum = "5b07eefe04486316c57aba08ab53dd44753c25102d1d3fe05775cc93a13262d9"
 dependencies = [
  "bitflags",
  "sentry-backtrace",
@@ -4777,16 +5220,16 @@ dependencies = [

 [[package]]
 name = "sentry-types"
-version = "0.48.3"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d7e78132ccfb4a1c777b959fb2944d1f6d5145bffe6be2a98ee6b25d244f72c"
+checksum = "567711f01f86a842057e1fc17779eba33a336004227e1a1e7e6cc2599e22e259"
 dependencies = [
  "debugid",
  "hex",
- "rand 0.9.4",
+ "rand 0.9.3",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "time",
  "url",
  "uuid",
@@ -4804,19 +5247,20 @@ dependencies = [

 [[package]]
 name = "serde-saphyr"
-version = "0.0.28"
+version = "0.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a15e3bc297370dc9c526bcf0986f29167a9286cfaad404f2f572b3074dabe5ec"
+checksum = "09fbdfe7a27a1b1633dfc0c4c8e65940b8d819c5ddb9cca48ebc3223b00c8b14"
 dependencies = [
  "ahash",
  "annotate-snippets",
  "base64 0.22.1",
  "encoding_rs_io",
  "getrandom 0.3.4",
- "granit-parser",
  "nohash-hasher",
  "num-traits",
- "serde_core",
+ "regex",
+ "saphyr-parser-bw",
+ "serde",
  "smallvec",
  "zmij",
 ]
@@ -4843,9 +5287,9 @@ dependencies = [

 [[package]]
 name = "serde_html_form"
-version = "0.4.0"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f"
+checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
 dependencies = [
  "form_urlencoded",
  "indexmap",
@@ -4856,9 +5300,9 @@ dependencies = [

 [[package]]
 name = "serde_json"
-version = "1.0.150"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
 dependencies = [
  "itoa",
  "memchr",
@@ -4880,9 +5324,9 @@ dependencies = [

 [[package]]
 name = "serde_regex"
-version = "1.2.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bafc8d0c5330cecff10f16b459b479fd9acaa5b4acd7167301414e21b0057012"
+checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
 dependencies = [
  "regex",
  "serde",
@@ -4926,18 +5370,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
- "digest 0.10.7",
-]
-
-[[package]]
-name = "sha1"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "digest 0.11.3",
+ "digest",
 ]

 [[package]]
@@ -4948,18 +5381,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
- "digest 0.10.7",
-]
-
-[[package]]
-name = "sha2"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "digest 0.11.3",
+ "digest",
 ]

 [[package]]
@@ -4971,7 +5393,7 @@ dependencies = [
  "async-trait",
  "bytes",
  "hex",
- "sha2 0.10.9",
+ "sha2",
  "tokio",
 ]

@@ -4990,12 +5412,6 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"

-[[package]]
-name = "shlex"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
-
 [[package]]
 name = "signal-hook"
 version = "0.3.18"
@@ -5043,26 +5459,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"

 [[package]]
-name = "simd_cesu8"
-version = "1.1.1"
+name = "simd_helpers"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
 dependencies = [
- "rustc_version",
- "simdutf8",
+ "quote",
 ]

-[[package]]
-name = "simdutf8"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
-
 [[package]]
 name = "siphasher"
-version = "1.0.3"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"

 [[package]]
 name = "slab"
@@ -5082,18 +5491,18 @@ dependencies = [

 [[package]]
 name = "smallvec"
-version = "1.15.2"
+version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 dependencies = [
  "serde",
 ]

 [[package]]
 name = "socket2"
-version = "0.6.4"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
 dependencies = [
  "libc",
  "windows-sys 0.61.2",
@@ -5161,6 +5570,15 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"

+[[package]]
+name = "subslice"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a8e4809a3bb02de01f1f7faf1ba01a83af9e8eabcd4d31dd6e413d14d56aae"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "subtle"
 version = "2.6.1"
@@ -5169,9 +5587,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"

 [[package]]
 name = "syn"
-version = "2.0.118"
+version = "2.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5198,27 +5616,6 @@ dependencies = [
  "syn",
 ]

-[[package]]
-name = "system-configuration"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
-dependencies = [
- "bitflags",
- "core-foundation 0.9.4",
- "system-configuration-sys",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
 [[package]]
 name = "tagptr"
 version = "0.2.0"
@@ -5248,7 +5645,7 @@ dependencies = [
  "lazy-regex",
  "minimad",
  "serde",
- "thiserror",
+ "thiserror 2.0.18",
  "unicode-width 0.1.14",
 ]

@@ -5262,13 +5659,33 @@ dependencies = [
  "pin-project",
 ]

+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
 [[package]]
 name = "thiserror"
 version = "2.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]

 [[package]]
@@ -5291,6 +5708,20 @@ dependencies = [
  "cfg-if",
 ]

+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
+]
+
 [[package]]
 name = "tikv-jemalloc-ctl"
 version = "0.6.1"
@@ -5321,11 +5752,12 @@ dependencies = [

 [[package]]
 name = "time"
-version = "0.3.49"
+version = "0.3.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
 dependencies = [
  "deranged",
+ "itoa",
  "num-conv",
  "powerfmt",
  "serde_core",
@@ -5335,15 +5767,15 @@ dependencies = [

 [[package]]
 name = "time-core"
-version = "0.1.9"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"

 [[package]]
 name = "time-macros"
-version = "0.2.29"
+version = "0.2.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
 dependencies = [
  "num-conv",
  "time-core",
@@ -5376,9 +5808,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"

 [[package]]
 name = "tokio"
-version = "1.52.3"
+version = "1.51.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
 dependencies = [
  "bytes",
  "libc",
@@ -5404,9 +5836,9 @@ dependencies = [

 [[package]]
 name = "tokio-metrics"
-version = "0.5.0"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9e81d53caf955549b1dec7af4ac2149e94cc25ed97b4a545151140281e2f528"
+checksum = "0e0410015c6db7b67b9c9ab2a3af4d74a942d637ff248d0d055073750deac6f9"
 dependencies = [
  "futures-util",
  "pin-project-lite",
@@ -5462,17 +5894,17 @@ dependencies = [

 [[package]]
 name = "toml"
-version = "1.1.2+spec-1.1.0"
+version = "0.9.12+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
 dependencies = [
  "indexmap",
  "serde_core",
  "serde_spanned 1.1.1",
- "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_datetime 0.7.5+spec-1.1.0",
  "toml_parser",
  "toml_writer",
- "winnow 1.0.3",
+ "winnow 0.7.15",
 ]

 [[package]]
@@ -5484,6 +5916,15 @@ dependencies = [
  "serde",
 ]

+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
 [[package]]
 name = "toml_datetime"
 version = "1.1.1+spec-1.1.0"
@@ -5509,14 +5950,14 @@ dependencies = [

 [[package]]
 name = "toml_edit"
-version = "0.25.12+spec-1.1.0"
+version = "0.25.11+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
 dependencies = [
  "indexmap",
  "toml_datetime 1.1.1+spec-1.1.0",
  "toml_parser",
- "winnow 1.0.3",
+ "winnow 1.0.1",
 ]

 [[package]]
@@ -5525,7 +5966,7 @@ version = "1.1.2+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
 dependencies = [
- "winnow 1.0.3",
+ "winnow 1.0.1",
 ]

 [[package]]
@@ -5542,9 +5983,9 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"

 [[package]]
 name = "tonic"
-version = "0.14.6"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
+checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
 dependencies = [
  "async-trait",
  "axum",
@@ -5571,26 +6012,15 @@ dependencies = [

 [[package]]
 name = "tonic-prost"
-version = "0.14.6"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
+checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
 dependencies = [
  "bytes",
  "prost",
  "tonic",
 ]

-[[package]]
-name = "tonic-types"
-version = "0.14.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8"
-dependencies = [
- "prost",
- "prost-types",
- "tonic",
-]
-
 [[package]]
 name = "tower"
 version = "0.5.3"
@@ -5610,27 +6040,11 @@ dependencies = [
  "tracing",
 ]

-[[package]]
-name = "tower-cookies"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
-dependencies = [
- "axum-core",
- "cookie",
- "futures-util",
- "http",
- "parking_lot",
- "pin-project-lite",
- "tower-layer",
- "tower-service",
-]
-
 [[package]]
 name = "tower-http"
-version = "0.6.11"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
 dependencies = [
  "async-compression",
  "bitflags",
@@ -5640,30 +6054,7 @@ dependencies = [
  "http",
  "http-body",
  "http-body-util",
- "pin-project-lite",
- "tokio",
- "tokio-util",
- "tower",
- "tower-layer",
- "tower-service",
- "url",
-]
-
-[[package]]
-name = "tower-http"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
-dependencies = [
- "async-compression",
- "bitflags",
- "bytes",
- "futures-core",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "percent-encoding",
+ "iri-string",
  "pin-project-lite",
  "tokio",
  "tokio-util",
@@ -5697,44 +6088,6 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"

-[[package]]
-name = "tower-sessions"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "518dca34b74a17cadfcee06e616a09d2bd0c3984eff1769e1e76d58df978fc78"
-dependencies = [
- "async-trait",
- "http",
- "time",
- "tokio",
- "tower-cookies",
- "tower-layer",
- "tower-service",
- "tower-sessions-core",
- "tracing",
-]
-
-[[package]]
-name = "tower-sessions-core"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "568531ec3dfcf3ffe493de1958ae5662a0284ac5d767476ecdb6a34ff8c6b06c"
-dependencies = [
- "async-trait",
- "axum-core",
- "base64 0.22.1",
- "futures",
- "http",
- "parking_lot",
- "rand 0.9.4",
- "serde",
- "serde_json",
- "thiserror",
- "time",
- "tokio",
- "tracing",
-]
-
 [[package]]
 name = "tracing"
 version = "0.1.44"
@@ -5803,9 +6156,9 @@ dependencies = [

 [[package]]
 name = "tracing-opentelemetry"
-version = "0.33.0"
+version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26"
+checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
 dependencies = [
  "js-sys",
  "opentelemetry",
@@ -5843,15 +6196,24 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"

 [[package]]
 name = "typenum"
-version = "1.20.1"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"

 [[package]]
 name = "typewit"
-version = "1.15.2"
+version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"
+checksum = "bc19094686c694eb41b3b99dcc2f2975d4b078512fa22ae6c63f7ca318bdcff7"
+dependencies = [
+ "typewit_proc_macros",
+]
+
+[[package]]
+name = "typewit_proc_macros"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"

 [[package]]
 name = "uname"
@@ -5885,9 +6247,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

 [[package]]
 name = "unicode-segmentation"
-version = "1.13.3"
+version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"

 [[package]]
 name = "unicode-width"
@@ -5913,6 +6275,34 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"

+[[package]]
+name = "ureq"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
+dependencies = [
+ "base64 0.22.1",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
+dependencies = [
+ "base64 0.22.1",
+ "http",
+ "httparse",
+ "log",
+]
+
 [[package]]
 name = "url"
 version = "2.5.8"
@@ -5938,6 +6328,12 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"

+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
 [[package]]
 name = "utf8_iter"
 version = "1.0.4"
@@ -5946,9 +6342,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"

 [[package]]
 name = "uuid"
-version = "1.23.3"
+version = "1.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
 dependencies = [
  "getrandom 0.4.2",
  "js-sys",
@@ -5956,6 +6352,17 @@ dependencies = [
  "wasm-bindgen",
 ]

+[[package]]
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "validator"
 version = "0.20.0"
@@ -6031,11 +6438,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"

 [[package]]
 name = "wasip2"
-version = "1.0.4+wasi-0.2.12"
+version = "1.0.2+wasi-0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
 dependencies = [
- "wit-bindgen 0.57.1",
+ "wit-bindgen",
 ]

 [[package]]
@@ -6044,14 +6451,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
 dependencies = [
- "wit-bindgen 0.51.0",
+ "wit-bindgen",
 ]

 [[package]]
 name = "wasm-bindgen"
-version = "0.2.125"
+version = "0.2.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -6062,9 +6469,9 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.75"
+version = "0.4.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
+checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6072,9 +6479,9 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.125"
+version = "0.2.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -6082,9 +6489,9 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.125"
+version = "0.2.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
 dependencies = [
  "bumpalo",
  "proc-macro2",
@@ -6095,9 +6502,9 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.125"
+version = "0.2.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
 dependencies = [
  "unicode-ident",
 ]
@@ -6126,9 +6533,9 @@ dependencies = [

 [[package]]
 name = "wasm-streams"
-version = "0.5.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
 dependencies = [
  "futures-util",
  "js-sys",
@@ -6151,9 +6558,9 @@ dependencies = [

 [[package]]
 name = "web-sys"
-version = "0.3.102"
+version = "0.3.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6183,9 +6590,18 @@ dependencies = [

 [[package]]
 name = "webpki-root-certs"
-version = "1.0.7"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
+checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
 dependencies = [
  "rustls-pki-types",
 ]
@@ -6274,6 +6690,15 @@ dependencies = [
  "windows-link",
 ]

+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.52.0"
@@ -6301,6 +6726,21 @@ dependencies = [
  "windows-link",
 ]

+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.52.6"
@@ -6334,6 +6774,12 @@ dependencies = [
  "windows_x86_64_msvc 0.53.1",
 ]

+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
@@ -6346,6 +6792,12 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"

+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
@@ -6358,6 +6810,12 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"

+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
@@ -6382,6 +6840,12 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"

+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
@@ -6394,6 +6858,12 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"

+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
@@ -6406,6 +6876,12 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"

+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
@@ -6418,6 +6894,12 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"

+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"
@@ -6441,9 +6923,9 @@ dependencies = [

 [[package]]
 name = "winnow"
-version = "1.0.3"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
 dependencies = [
  "memchr",
 ]
@@ -6457,12 +6939,6 @@ dependencies = [
  "wit-bindgen-rust-macro",
 ]

-[[package]]
-name = "wit-bindgen"
-version = "0.57.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-
 [[package]]
 name = "wit-bindgen-core"
 version = "0.51.0"
@@ -6548,6 +7024,23 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"

+[[package]]
+name = "x509-parser"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom 7.1.3",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]
+
 [[package]]
 name = "xml5ever"
 version = "0.18.1"
@@ -6561,7 +7054,7 @@ dependencies = [

 [[package]]
 name = "xtask"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "askama",
  "cargo_metadata",
@@ -6570,6 +7063,12 @@ dependencies = [
  "conduwuit_admin",
 ]

+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
 [[package]]
 name = "yansi"
 version = "1.0.1"
@@ -6578,9 +7077,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

 [[package]]
 name = "yoke"
-version = "0.8.3"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
 dependencies = [
  "stable_deref_trait",
  "yoke-derive",
@@ -6601,18 +7100,18 @@ dependencies = [

 [[package]]
 name = "zerocopy"
-version = "0.8.52"
+version = "0.8.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
 dependencies = [
  "zerocopy-derive",
 ]

 [[package]]
 name = "zerocopy-derive"
-version = "0.8.52"
+version = "0.8.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6621,9 +7120,9 @@ dependencies = [

 [[package]]
 name = "zerofrom"
-version = "0.1.8"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
 dependencies = [
  "zerofrom-derive",
 ]
@@ -6642,9 +7141,9 @@ dependencies = [

 [[package]]
 name = "zeroize"
-version = "1.9.0"
+version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"

 [[package]]
 name = "zerotrie"
@@ -6719,6 +7218,15 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"

+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
 [[package]]
 name = "zune-jpeg"
 version = "0.5.15"
2026-07-03 10:59:11 +02:00
theS1LV3R a66267b495 chore: Add news fragment 2026-07-03 10:59:11 +02:00
theS1LV3R 3dbe6a5817 fix: Make clippy not be angry 2026-07-03 10:59:11 +02:00
theS1LV3R b5cd069d85 refactor: Rewrite resolver with resolvematrix
diff --git c/Cargo.lock i/Cargo.lock
index 081dc19e7..5b6b6fe4e 100644
--- c/Cargo.lock
+++ i/Cargo.lock
@@ -39,6 +39,24 @@ dependencies = [
  "memchr",
 ]

+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
 [[package]]
 name = "alloc-no-stdlib"
 version = "2.0.4"
@@ -47,9 +65,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"

 [[package]]
 name = "alloc-stdlib"
-version = "0.2.4"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
 dependencies = [
  "alloc-no-stdlib",
 ]
@@ -71,9 +89,9 @@ dependencies = [

 [[package]]
 name = "annotate-snippets"
-version = "0.12.16"
+version = "0.12.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1"
+checksum = "74fc7650eedcb2fee505aad48491529e408f0e854c2d9f63eb86c1361b9b3f93"
 dependencies = [
  "anstyle",
  "memchr",
@@ -102,14 +120,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"

 [[package]]
-name = "arc-swap"
-version = "1.9.1"
+name = "ar_archive_writer"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
+checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+dependencies = [
+ "object",
+]
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "arc-swap"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
 dependencies = [
  "rustversion",
 ]

+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "argon2"
 version = "0.5.3"
@@ -137,6 +181,15 @@ dependencies = [
  "serde",
 ]

+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
 [[package]]
 name = "as_variant"
 version = "1.3.0"
@@ -145,9 +198,9 @@ checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a"

 [[package]]
 name = "askama"
-version = "0.16.0"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc"
+checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57"
 dependencies = [
  "askama_macros",
  "itoa",
@@ -158,13 +211,12 @@ dependencies = [

 [[package]]
 name = "askama_derive"
-version = "0.16.0"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738"
+checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37"
 dependencies = [
  "askama_parser",
  "basic-toml",
- "glob",
  "memchr",
  "proc-macro2",
  "quote",
@@ -176,24 +228,63 @@ dependencies = [

 [[package]]
 name = "askama_macros"
-version = "0.16.0"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a"
+checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b"
 dependencies = [
  "askama_derive",
 ]

 [[package]]
 name = "askama_parser"
-version = "0.16.0"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da"
+checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c"
 dependencies = [
  "rustc-hash",
  "serde",
  "serde_derive",
  "unicode-ident",
- "winnow 1.0.3",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "asn1-rs"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]

 [[package]]
@@ -215,9 +306,9 @@ dependencies = [

 [[package]]
 name = "async-compression"
-version = "0.4.42"
+version = "0.4.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
 dependencies = [
  "compression-codecs",
  "compression-core",
@@ -253,15 +344,58 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"

 [[package]]
 name = "autocfg"
-version = "1.5.1"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.18",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom 8.0.0",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
+dependencies = [
+ "arrayvec",
+]

 [[package]]
 name = "aws-lc-rs"
-version = "1.17.0"
+version = "1.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
+checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
 dependencies = [
  "aws-lc-sys",
  "zeroize",
@@ -269,9 +403,9 @@ dependencies = [

 [[package]]
 name = "aws-lc-sys"
-version = "0.41.0"
+version = "0.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
+checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
 dependencies = [
  "cc",
  "cmake",
@@ -281,9 +415,9 @@ dependencies = [

 [[package]]
 name = "axum"
-version = "0.8.9"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
 dependencies = [
  "axum-core",
  "bytes",
@@ -314,12 +448,12 @@ dependencies = [

 [[package]]
 name = "axum-client-ip"
-version = "1.3.1"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8ba1af5b620232acf37f2eb6d22151ea465491e0b4c25f552d1990f64ec5a67"
+checksum = "dff8ee1869817523c8f91c20bf17fd932707f66c2e7e0b0f811b29a227289562"
 dependencies = [
  "axum",
- "client-ip",
+ "forwarded-header-value",
  "serde",
 ]

@@ -344,9 +478,9 @@ dependencies = [

 [[package]]
 name = "axum-extra"
-version = "0.12.6"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
+checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
 dependencies = [
  "axum",
  "axum-core",
@@ -367,13 +501,12 @@ dependencies = [

 [[package]]
 name = "axum-server"
-version = "0.8.0"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc"
+checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9"
 dependencies = [
  "arc-swap",
  "bytes",
- "either",
  "fs-err",
  "http",
  "http-body",
@@ -381,6 +514,7 @@ dependencies = [
  "hyper-util",
  "pin-project-lite",
  "rustls",
+ "rustls-pemfile",
  "rustls-pki-types",
  "tokio",
  "tokio-rustls",
@@ -389,8 +523,9 @@ dependencies = [

 [[package]]
 name = "axum-server-dual-protocol"
-version = "0.8.0"
-source = "git+https://github.com/vinchona/axum-server-dual-protocol.git?rev=ca6db055254255b74238673ce4135698e347d71c#ca6db055254255b74238673ce4135698e347d71c"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2164551db024e87f20316d164eab9f5ad342d8188b08051ceb15ca92a60ea7b7"
 dependencies = [
  "axum-server",
  "bytes",
@@ -461,15 +596,30 @@ dependencies = [
  "quote",
  "regex",
  "rustc-hash",
- "shlex 1.3.0",
+ "shlex",
  "syn",
 ]

 [[package]]
-name = "bitflags"
-version = "2.13.0"
+name = "bit_field"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "bitstream-io"
+version = "4.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
+dependencies = [
+ "core2",
+]

 [[package]]
 name = "blake2"
@@ -477,7 +627,7 @@ version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
 dependencies = [
- "digest 0.10.7",
+ "digest",
 ]

 [[package]]
@@ -489,15 +639,6 @@ dependencies = [
  "generic-array",
 ]

-[[package]]
-name = "block-buffer"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
-dependencies = [
- "hybrid-array",
-]
-
 [[package]]
 name = "block2"
 version = "0.6.2"
@@ -508,10 +649,19 @@ dependencies = [
 ]

 [[package]]
-name = "brotli"
-version = "8.0.4"
+name = "blurhash"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc"
+dependencies = [
+ "image",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -520,9 +670,9 @@ dependencies = [

 [[package]]
 name = "brotli-decompressor"
-version = "5.0.3"
+version = "5.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -530,15 +680,15 @@ dependencies = [

 [[package]]
 name = "built"
-version = "0.8.1"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"

 [[package]]
 name = "bumpalo"
-version = "3.20.3"
+version = "3.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"

 [[package]]
 name = "bytemuck"
@@ -560,15 +710,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"

 [[package]]
 name = "bytes"
-version = "1.12.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"

 [[package]]
 name = "bytesize"
-version = "2.4.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d"
+checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"

 [[package]]
 name = "bzip2-sys"
@@ -591,9 +741,9 @@ dependencies = [

 [[package]]
 name = "cargo-platform"
-version = "0.3.3"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba"
+checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082"
 dependencies = [
  "serde",
  "serde_core",
@@ -610,7 +760,7 @@ dependencies = [
  "semver",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
 ]

 [[package]]
@@ -625,16 +775,22 @@ dependencies = [

 [[package]]
 name = "cc"
-version = "1.2.64"
+version = "1.2.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
+checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
  "libc",
- "shlex 2.0.1",
+ "shlex",
 ]

+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
 [[package]]
 name = "cexpr"
 version = "0.6.0"
@@ -664,7 +820,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.3.0",
- "rand_core 0.10.1",
+ "rand_core 0.10.0",
 ]

 [[package]]
@@ -678,13 +834,23 @@ dependencies = [

 [[package]]
 name = "chrono"
-version = "0.4.45"
+version = "0.4.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
 dependencies = [
  "num-traits",
 ]

+[[package]]
+name = "chumsky"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
+dependencies = [
+ "hashbrown 0.14.5",
+ "stacker",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.8.1"
@@ -698,9 +864,9 @@ dependencies = [

 [[package]]
 name = "clap"
-version = "4.6.1"
+version = "4.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -718,9 +884,9 @@ dependencies = [

 [[package]]
 name = "clap_derive"
-version = "4.6.1"
+version = "4.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -734,30 +900,15 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

-[[package]]
-name = "client-ip"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39d2056bf065c8b4bce5a8898d40e175211ff4410add2a84d695845d3937c729"
-dependencies = [
- "http",
-]
-
 [[package]]
 name = "cmake"
-version = "0.1.58"
+version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
 dependencies = [
  "cc",
 ]

-[[package]]
-name = "cmov"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
-
 [[package]]
 name = "color_quant"
 version = "1.1.0"
@@ -776,9 +927,9 @@ dependencies = [

 [[package]]
 name = "compression-codecs"
-version = "0.4.38"
+version = "0.4.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
 dependencies = [
  "brotli",
  "compression-core",
@@ -790,9 +941,9 @@ dependencies = [

 [[package]]
 name = "compression-core"
-version = "0.4.32"
+version = "0.4.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"

 [[package]]
 name = "concurrent-queue"
@@ -805,30 +956,24 @@ dependencies = [

 [[package]]
 name = "conduwuit"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "aws-lc-rs",
  "clap",
  "conduwuit_admin",
  "conduwuit_api",
  "conduwuit_build_metadata",
  "conduwuit_core",
  "conduwuit_database",
- "conduwuit_macros",
  "conduwuit_router",
  "conduwuit_service",
  "console-subscriber",
  "const-str",
- "ctor",
- "dtor",
  "hardened_malloc-rs",
  "log",
  "opentelemetry",
  "opentelemetry-otlp",
  "opentelemetry_sdk",
  "parking_lot",
- "reqwest 0.13.4",
- "rustls",
  "sentry",
  "sentry-tower",
  "sentry-tracing",
@@ -843,9 +988,8 @@ dependencies = [

 [[package]]
 name = "conduwuit_admin"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "assign",
  "clap",
  "conduwuit_api",
  "conduwuit_core",
@@ -853,11 +997,10 @@ dependencies = [
  "conduwuit_macros",
  "conduwuit_service",
  "const-str",
- "ctor",
- "dtor",
  "futures",
  "lettre",
  "log",
+ "resolvematrix",
  "ruma",
  "serde-saphyr",
  "serde_json",
@@ -868,9 +1011,8 @@ dependencies = [

 [[package]]
 name = "conduwuit_api"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "assign",
  "async-trait",
  "axum",
  "axum-client-ip",
@@ -878,47 +1020,41 @@ dependencies = [
  "base64 0.22.1",
  "bytes",
  "conduwuit_core",
- "conduwuit_macros",
  "conduwuit_service",
  "const-str",
- "ctor",
- "dtor",
  "futures",
  "hmac",
  "http",
  "http-body-util",
  "hyper",
  "ipaddress",
- "itertools 0.15.0",
+ "itertools 0.14.0",
  "lettre",
  "log",
- "rand 0.10.1",
- "reqwest 0.13.4",
+ "rand 0.10.0",
+ "reqwest 0.12.28",
  "ruma",
- "ruminuwuity",
  "serde",
  "serde_html_form",
  "serde_json",
- "sha1 0.11.0",
+ "sha1",
  "tokio",
  "tracing",
 ]

 [[package]]
 name = "conduwuit_build_metadata"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "built",
- "cargo_metadata",
 ]

 [[package]]
 name = "conduwuit_core"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "argon2",
  "arrayvec",
- "assign",
  "axum",
  "axum-extra",
  "bytes",
@@ -933,7 +1069,6 @@ dependencies = [
  "core_affinity",
  "ctor",
  "cyborgtime",
- "dtor",
  "either",
  "figment",
  "futures",
@@ -942,36 +1077,37 @@ dependencies = [
  "http-body-util",
  "hyper-util",
  "ipaddress",
- "itertools 0.15.0",
+ "itertools 0.14.0",
  "lettre",
  "libc",
  "libloading 0.9.0",
  "lock_api",
  "log",
  "maplit",
- "nix",
+ "nix 0.31.2",
  "num-traits",
  "parking_lot",
- "rand 0.10.1",
+ "rand 0.10.0",
  "rand_core 0.6.4",
  "regex",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
+ "resolvematrix",
+ "ring",
  "ruma",
  "sanitize-filename",
  "serde",
  "serde-saphyr",
  "serde_json",
  "serde_regex",
- "sha2 0.11.0",
  "smallstr",
  "smallvec",
- "thiserror",
+ "thiserror 2.0.18",
  "tikv-jemalloc-ctl",
  "tikv-jemalloc-sys",
  "tikv-jemallocator",
  "tokio",
  "tokio-metrics",
- "toml 1.1.2+spec-1.1.0",
+ "toml 0.9.12+spec-1.1.0",
  "tracing",
  "tracing-core",
  "tracing-subscriber",
@@ -980,14 +1116,11 @@ dependencies = [

 [[package]]
 name = "conduwuit_database"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "async-channel",
  "conduwuit_core",
- "conduwuit_macros",
  "const-str",
- "ctor",
- "dtor",
  "futures",
  "log",
  "minicbor",
@@ -1001,10 +1134,9 @@ dependencies = [

 [[package]]
 name = "conduwuit_macros"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "cargo_toml",
- "itertools 0.15.0",
+ "itertools 0.14.0",
  "proc-macro2",
  "quote",
  "syn",
@@ -1012,9 +1144,8 @@ dependencies = [

 [[package]]
 name = "conduwuit_router"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
- "assign",
  "axum",
  "axum-client-ip",
  "axum-server",
@@ -1023,12 +1154,9 @@ dependencies = [
  "conduwuit_admin",
  "conduwuit_api",
  "conduwuit_core",
- "conduwuit_macros",
  "conduwuit_service",
  "conduwuit_web",
  "const-str",
- "ctor",
- "dtor",
  "futures",
  "http",
  "http-body-util",
@@ -1044,25 +1172,22 @@ dependencies = [
  "serde_json",
  "tokio",
  "tower",
- "tower-http 0.7.0",
+ "tower-http",
  "tracing",
 ]

 [[package]]
 name = "conduwuit_service"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "askama",
- "assign",
  "async-trait",
  "base64 0.22.1",
+ "blurhash",
  "bytes",
  "conduwuit_core",
  "conduwuit_database",
- "conduwuit_macros",
  "const-str",
- "ctor",
- "dtor",
  "either",
  "futures",
  "governor",
@@ -1070,25 +1195,25 @@ dependencies = [
  "http",
  "image",
  "ipaddress",
- "itertools 0.15.0",
+ "itertools 0.14.0",
+ "ldap3",
  "lettre",
  "log",
  "loole",
  "lru-cache",
  "nonzero_ext",
- "rand 0.10.1",
+ "rand 0.10.0",
  "recaptcha-verify",
  "regex",
  "reqwest 0.12.28",
- "reqwest 0.13.4",
+ "resolvematrix",
  "ruma",
- "ruminuwuity",
  "rustyline-async",
  "sd-notify",
  "serde",
  "serde-saphyr",
  "serde_json",
- "sha2 0.11.0",
+ "sha2",
  "termimad",
  "tokio",
  "tracing",
@@ -1099,10 +1224,9 @@ dependencies = [

 [[package]]
 name = "conduwuit_web"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "askama",
- "assign",
  "async-trait",
  "axum",
  "axum-extra",
@@ -1112,11 +1236,11 @@ dependencies = [
  "conduwuit_service",
  "futures",
  "memory-serve",
- "rand 0.10.1",
+ "rand 0.10.0",
  "ruma",
  "serde",
- "thiserror",
- "tower-http 0.7.0",
+ "thiserror 2.0.18",
+ "tower-http",
  "tower-sec-fetch",
  "tracing",
  "validator",
@@ -1170,9 +1294,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"

 [[package]]
 name = "const-str"
-version = "1.1.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
+checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160"

 [[package]]
 name = "const_panic"
@@ -1183,6 +1307,16 @@ dependencies = [
  "typewit",
 ]

+[[package]]
+name = "continuwuity-admin-api"
+version = "0.1.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
+dependencies = [
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "convert_case"
 version = "0.10.0"
@@ -1212,16 +1346,6 @@ dependencies = [
  "crossterm",
 ]

-[[package]]
-name = "core-foundation"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
 [[package]]
 name = "core-foundation"
 version = "0.10.1"
@@ -1238,6 +1362,15 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"

+[[package]]
+name = "core2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "core_affinity"
 version = "0.8.3"
@@ -1391,6 +1524,12 @@ dependencies = [
  "winapi",
 ]

+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
 [[package]]
 name = "crypto-common"
 version = "0.1.7"
@@ -1401,33 +1540,21 @@ dependencies = [
  "typenum",
 ]

-[[package]]
-name = "crypto-common"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
-dependencies = [
- "hybrid-array",
-]
-
 [[package]]
 name = "ctor"
-version = "1.0.7"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a"
+checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
 dependencies = [
- "link-section",
- "linktime-proc-macro",
+ "ctor-proc-macro",
+ "dtor",
 ]

 [[package]]
-name = "ctutils"
-version = "0.4.2"
+name = "ctor-proc-macro"
+version = "0.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
-dependencies = [
- "cmov",
-]
+checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"

 [[package]]
 name = "curve25519-dalek"
@@ -1438,7 +1565,7 @@ dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
  "curve25519-dalek-derive",
- "digest 0.10.7",
+ "digest",
  "fiat-crypto",
  "rustc_version",
  "subtle",
@@ -1499,9 +1626,9 @@ dependencies = [

 [[package]]
 name = "data-encoding"
-version = "2.11.0"
+version = "2.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"

 [[package]]
 name = "date_header"
@@ -1529,11 +1656,28 @@ dependencies = [
  "zeroize",
 ]

+[[package]]
+name = "der-parser"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
 [[package]]
 name = "deranged"
 version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]

 [[package]]
 name = "derive_more"
@@ -1563,22 +1707,11 @@ version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
- "block-buffer 0.10.4",
- "crypto-common 0.1.7",
+ "block-buffer",
+ "crypto-common",
  "subtle",
 ]

-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer 0.12.1",
- "crypto-common 0.2.2",
- "ctutils",
-]
-
 [[package]]
 name = "dispatch2"
 version = "0.3.1"
@@ -1591,9 +1724,9 @@ dependencies = [

 [[package]]
 name = "displaydoc"
-version = "0.2.6"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1610,14 +1743,30 @@ dependencies = [
 ]

 [[package]]
-name = "dtor"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d738e43aa64edab57c983d56de890d65fea7dc05605490c74451ce721dfd84b"
+name = "draupnir-antispam"
+version = "0.1.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
- "linktime-proc-macro",
+ "ruma-common",
+ "serde",
+ "serde_json",
 ]

+[[package]]
+name = "dtor"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301"
+dependencies = [
+ "dtor-proc-macro",
+]
+
+[[package]]
+name = "dtor-proc-macro"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
 [[package]]
 name = "dunce"
 version = "1.0.5"
@@ -1642,17 +1791,18 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
+ "rand_core 0.6.4",
  "serde",
- "sha2 0.10.9",
+ "sha2",
  "subtle",
  "zeroize",
 ]

 [[package]]
 name = "either"
-version = "1.16.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 dependencies = [
  "serde",
 ]
@@ -1691,6 +1841,38 @@ dependencies = [
  "encoding_rs",
 ]

+[[package]]
+name = "enum-as-inner"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.2"
@@ -1727,10 +1909,45 @@ dependencies = [
 ]

 [[package]]
-name = "fastrand"
-version = "2.4.1"
+name = "exr"
+version = "1.74.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fax"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
+dependencies = [
+ "fax_derive",
+]
+
+[[package]]
+name = "fax_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]

 [[package]]
 name = "fdeflate"
@@ -1822,6 +2039,16 @@ dependencies = [
  "percent-encoding",
 ]

+[[package]]
+name = "forwarded-header-value"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
+dependencies = [
+ "nonempty",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "fs-err"
 version = "3.3.0"
@@ -1921,9 +2148,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"

 [[package]]
 name = "futures-timer"
-version = "3.0.4"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"

 [[package]]
 name = "futures-util"
@@ -1988,16 +2215,16 @@ dependencies = [
  "cfg-if",
  "libc",
  "r-efi 6.0.0",
- "rand_core 0.10.1",
+ "rand_core 0.10.0",
  "wasip2",
  "wasip3",
 ]

 [[package]]
 name = "gif"
-version = "0.14.2"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
+checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
 dependencies = [
  "color_quant",
  "weezl",
@@ -2034,21 +2261,11 @@ dependencies = [
  "web-time",
 ]

-[[package]]
-name = "granit-parser"
-version = "0.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f50ba32164f9e098d5da618776a32afbb32270adcbe3d3d006107dae11e37c91"
-dependencies = [
- "arraydeque",
- "smallvec",
-]
-
 [[package]]
 name = "h2"
-version = "0.4.15"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2091,12 +2308,33 @@ dependencies = [
  "tokio-util",
 ]

+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
 [[package]]
 name = "hardened_malloc-rs"
 version = "0.1.2+12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "647deb1583b14d160f85f3ff626f20b6edd366e3852c9843b06077388f794cb6"

+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.15.5"
@@ -2117,12 +2355,6 @@ dependencies = [
  "foldhash 0.2.0",
 ]

-[[package]]
-name = "hashbrown"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
-
 [[package]]
 name = "hdrhistogram"
 version = "7.5.4"
@@ -2148,7 +2380,7 @@ dependencies = [
  "http",
  "httpdate",
  "mime",
- "sha1 0.10.6",
+ "sha1",
 ]

 [[package]]
@@ -2179,84 +2411,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"

 [[package]]
-name = "hickory-net"
-version = "0.26.1"
+name = "hickory-proto"
+version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
+checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
 dependencies = [
  "async-trait",
  "cfg-if",
  "data-encoding",
+ "enum-as-inner",
  "futures-channel",
  "futures-io",
  "futures-util",
- "hickory-proto",
  "idna",
  "ipnet",
- "jni",
- "rand 0.10.1",
- "thiserror",
- "tinyvec",
- "tokio",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "hickory-proto"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
-dependencies = [
- "data-encoding",
- "idna",
- "ipnet",
- "jni",
  "once_cell",
- "prefix-trie",
- "rand 0.10.1",
+ "rand 0.9.2",
  "ring",
  "serde",
- "thiserror",
+ "thiserror 2.0.18",
  "tinyvec",
+ "tokio",
  "tracing",
  "url",
 ]

 [[package]]
 name = "hickory-resolver"
-version = "0.26.1"
+version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
+checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
 dependencies = [
  "cfg-if",
  "futures-util",
- "hickory-net",
  "hickory-proto",
  "ipconfig",
- "ipnet",
- "jni",
  "moka",
- "ndk-context",
  "once_cell",
  "parking_lot",
- "rand 0.10.1",
+ "rand 0.9.2",
  "resolv-conf",
  "serde",
  "smallvec",
- "system-configuration",
- "thiserror",
+ "thiserror 2.0.18",
  "tokio",
  "tracing",
 ]

 [[package]]
 name = "hmac"
-version = "0.13.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
- "digest 0.11.3",
+ "digest",
 ]

 [[package]]
@@ -2286,9 +2494,9 @@ dependencies = [

 [[package]]
 name = "http"
-version = "1.4.2"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
 dependencies = [
  "bytes",
  "itoa",
@@ -2344,20 +2552,11 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"

-[[package]]
-name = "hybrid-array"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
-dependencies = [
- "typenum",
-]
-
 [[package]]
 name = "hyper"
-version = "1.10.1"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2370,6 +2569,7 @@ dependencies = [
  "httpdate",
  "itoa",
  "pin-project-lite",
+ "pin-utils",
  "smallvec",
  "tokio",
  "want",
@@ -2377,18 +2577,20 @@ dependencies = [

 [[package]]
 name = "hyper-rustls"
-version = "0.27.9"
+version = "0.27.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
 dependencies = [
  "http",
  "hyper",
  "hyper-util",
  "rustls",
  "rustls-native-certs",
+ "rustls-pki-types",
  "tokio",
  "tokio-rustls",
  "tower-service",
+ "webpki-roots",
 ]

 [[package]]
@@ -2420,7 +2622,7 @@ dependencies = [
  "libc",
  "percent-encoding",
  "pin-project-lite",
- "socket2",
+ "socket2 0.6.3",
  "tokio",
  "tower-service",
  "tracing",
@@ -2428,13 +2630,12 @@ dependencies = [

 [[package]]
 name = "icu_collections"
-version = "2.2.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
 dependencies = [
  "displaydoc",
  "potential_utf",
- "utf8_iter",
  "yoke",
  "zerofrom",
  "zerovec",
@@ -2442,9 +2643,9 @@ dependencies = [

 [[package]]
 name = "icu_locale_core"
-version = "2.2.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
 dependencies = [
  "displaydoc",
  "litemap",
@@ -2455,9 +2656,9 @@ dependencies = [

 [[package]]
 name = "icu_normalizer"
-version = "2.2.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
 dependencies = [
  "icu_collections",
  "icu_normalizer_data",
@@ -2469,15 +2670,15 @@ dependencies = [

 [[package]]
 name = "icu_normalizer_data"
-version = "2.2.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"

 [[package]]
 name = "icu_properties"
-version = "2.2.0"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
 dependencies = [
  "icu_collections",
  "icu_locale_core",
@@ -2489,15 +2690,15 @@ dependencies = [

 [[package]]
 name = "icu_properties_data"
-version = "2.2.0"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"

 [[package]]
 name = "icu_provider"
-version = "2.2.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
 dependencies = [
  "displaydoc",
  "icu_locale_core",
@@ -2533,9 +2734,9 @@ dependencies = [

 [[package]]
 name = "idna_adapter"
-version = "1.2.2"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
 dependencies = [
  "icu_normalizer",
  "icu_properties",
@@ -2550,11 +2751,17 @@ dependencies = [
  "bytemuck",
  "byteorder-lite",
  "color_quant",
+ "exr",
  "gif",
  "image-webp",
  "moxcms",
  "num-traits",
  "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
  "zune-core",
  "zune-jpeg",
 ]
@@ -2570,13 +2777,19 @@ dependencies = [
 ]

 [[package]]
-name = "indexmap"
-version = "2.14.0"
+name = "imgref"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
 dependencies = [
  "equivalent",
- "hashbrown 0.17.1",
+ "hashbrown 0.16.1",
  "serde",
  "serde_core",
 ]
@@ -2587,6 +2800,17 @@ version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"

+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "ipaddress"
 version = "0.1.3"
@@ -2603,15 +2827,14 @@ dependencies = [

 [[package]]
 name = "ipconfig"
-version = "0.3.4"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
+checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
 dependencies = [
- "socket2",
+ "socket2 0.5.10",
  "widestring",
- "windows-registry",
- "windows-result",
- "windows-sys 0.61.2",
+ "windows-sys 0.48.0",
+ "winreg",
 ]

 [[package]]
@@ -2619,7 +2842,14 @@ name = "ipnet"
 version = "2.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
 dependencies = [
+ "memchr",
  "serde",
 ]

@@ -2641,49 +2871,35 @@ dependencies = [
  "either",
 ]

-[[package]]
-name = "itertools"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itoa"
-version = "1.0.18"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"

 [[package]]
 name = "jni"
-version = "0.22.4"
+version = "0.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
 dependencies = [
+ "cesu8",
  "cfg-if",
  "combine",
- "jni-macros",
- "jni-sys",
+ "jni-sys 0.3.1",
  "log",
- "simd_cesu8",
- "thiserror",
+ "thiserror 1.0.69",
  "walkdir",
- "windows-link",
+ "windows-sys 0.45.0",
 ]

 [[package]]
-name = "jni-macros"
-version = "0.22.4"
+name = "jni-sys"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
 dependencies = [
- "proc-macro2",
- "quote",
- "rustc_version",
- "simd_cesu8",
- "syn",
+ "jni-sys 0.4.1",
 ]

 [[package]]
@@ -2717,12 +2933,11 @@ dependencies = [

 [[package]]
 name = "js-sys"
-version = "0.3.102"
+version = "0.3.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
 dependencies = [
- "cfg-if",
- "futures-util",
+ "once_cell",
  "wasm-bindgen",
 ]

@@ -2737,20 +2952,30 @@ dependencies = [

 [[package]]
 name = "js_option"
-version = "0.2.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c"
+checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c"
 dependencies = [
- "serde_core",
+ "serde",
 ]

 [[package]]
 name = "konst"
-version = "0.4.3"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d"
+checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9"
 dependencies = [
  "const_panic",
+ "konst_kernel",
+ "typewit",
+]
+
+[[package]]
+name = "konst_kernel"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c"
+dependencies = [
  "typewit",
 ]

@@ -2783,6 +3008,41 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"

+[[package]]
+name = "lber"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbcf559624bfd9fe8d488329a8959766335a43a9b8b2cdd6a2c379fca02909a5"
+dependencies = [
+ "bytes",
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "ldap3"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01fe89f5e7cfb7e4701e3a38ff9f00358e026a9aee940355d88ee9d81e5c7503"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures",
+ "futures-util",
+ "lber",
+ "log",
+ "nom 7.1.3",
+ "percent-encoding",
+ "rustls",
+ "rustls-native-certs",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tokio-util",
+ "url",
+ "x509-parser",
+]
+
 [[package]]
 name = "leb128fmt"
 version = "0.1.0"
@@ -2790,13 +3050,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"

 [[package]]
-name = "lettre"
-version = "0.11.22"
+name = "lebe"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
+[[package]]
+name = "lettre"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
+ "chumsky",
  "email-encoding",
  "email_address",
  "fastrand",
@@ -2812,7 +3079,7 @@ dependencies = [
  "rustls",
  "rustls-native-certs",
  "serde",
- "socket2",
+ "socket2 0.6.3",
  "tokio",
  "tokio-rustls",
  "tracing",
@@ -2821,9 +3088,19 @@ dependencies = [

 [[package]]
 name = "libc"
-version = "0.2.186"
+version = "0.2.183"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
+dependencies = [
+ "arbitrary",
+ "cc",
+]

 [[package]]
 name = "libloading"
@@ -2847,33 +3124,21 @@ dependencies = [

 [[package]]
 name = "libz-sys"
-version = "1.1.29"
+version = "1.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
+checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1"
 dependencies = [
  "cc",
  "pkg-config",
  "vcpkg",
 ]

-[[package]]
-name = "link-section"
-version = "0.18.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2b1dd6fe32e55c0fc0ea9493aa57459ca3cf4ff3c857c7d0302290150da6e4f"
-
 [[package]]
 name = "linked-hash-map"
 version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"

-[[package]]
-name = "linktime-proc-macro"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee"
-
 [[package]]
 name = "linux-raw-sys"
 version = "0.12.1"
@@ -2882,9 +3147,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"

 [[package]]
 name = "litemap"
-version = "0.8.2"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"

 [[package]]
 name = "litrs"
@@ -2903,9 +3168,9 @@ dependencies = [

 [[package]]
 name = "log"
-version = "0.4.32"
+version = "0.4.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"

 [[package]]
 name = "loole"
@@ -2917,6 +3182,15 @@ dependencies = [
  "futures-sink",
 ]

+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
 [[package]]
 name = "lru-cache"
 version = "0.1.2"
@@ -2996,10 +3270,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"

 [[package]]
-name = "memchr"
-version = "2.8.2"
+name = "maybe-rayon"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"

 [[package]]
 name = "memory-serve"
@@ -3017,6 +3301,16 @@ dependencies = [
  "walkdir",
 ]

+[[package]]
+name = "meowlnir-antispam"
+version = "0.1.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
+dependencies = [
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "mime"
 version = "0.3.17"
@@ -3035,15 +3329,15 @@ dependencies = [

 [[package]]
 name = "minicbor"
-version = "2.2.2"
+version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b7a5041e12946f8b7d3f5a9d96383a19d694b9335457c522be7815b9abafb02"
+checksum = "e70eae6d4f18f7d76877fe7b13f0bc21f7c2b7239d2041c338335f7b388d0dd7"

 [[package]]
 name = "minicbor-serde"
-version = "0.7.0"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "293c7245401f035e2dcc4b12ebdb5c9d8847247fc79fe1b5b0a0d58d7275324c"
+checksum = "80047f75e28e3b38f6ab2ec3c2c7669f6b411fa6f8424e1a90a3fd784b19a3f4"
 dependencies = [
  "minicbor",
  "serde",
@@ -3076,9 +3370,9 @@ dependencies = [

 [[package]]
 name = "mio"
-version = "1.2.1"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
 dependencies = [
  "libc",
  "log",
@@ -3088,9 +3382,9 @@ dependencies = [

 [[package]]
 name = "moka"
-version = "0.12.15"
+version = "0.12.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
+checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b"
 dependencies = [
  "crossbeam-channel",
  "crossbeam-epoch",
@@ -3113,12 +3407,6 @@ dependencies = [
  "pxfm",
 ]

-[[package]]
-name = "ndk-context"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
-
 [[package]]
 name = "new_debug_unreachable"
 version = "1.0.6"
@@ -3127,9 +3415,21 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"

 [[package]]
 name = "nix"
-version = "0.31.3"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nix"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
 dependencies = [
  "bitflags",
  "cfg-if",
@@ -3162,12 +3462,24 @@ dependencies = [
  "memchr",
 ]

+[[package]]
+name = "nonempty"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
+
 [[package]]
 name = "nonzero_ext"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"

+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
 [[package]]
 name = "nu-ansi-term"
 version = "0.50.3"
@@ -3212,9 +3524,20 @@ dependencies = [

 [[package]]
 name = "num-conv"
-version = "0.2.2"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]

 [[package]]
 name = "num-integer"
@@ -3434,6 +3757,15 @@ dependencies = [
  "memchr",
 ]

+[[package]]
+name = "oid-registry"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
+dependencies = [
+ "asn1-rs",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.21.4"
@@ -3452,36 +3784,36 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"

 [[package]]
 name = "opentelemetry"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682"
+checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0"
 dependencies = [
  "futures-core",
  "futures-sink",
  "js-sys",
  "pin-project-lite",
- "thiserror",
+ "thiserror 2.0.18",
  "tracing",
 ]

 [[package]]
 name = "opentelemetry-http"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331"
+checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d"
 dependencies = [
  "async-trait",
  "bytes",
  "http",
  "opentelemetry",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
 ]

 [[package]]
 name = "opentelemetry-otlp"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35"
+checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf"
 dependencies = [
  "http",
  "opentelemetry",
@@ -3489,18 +3821,18 @@ dependencies = [
  "opentelemetry-proto",
  "opentelemetry_sdk",
  "prost",
- "reqwest 0.13.4",
- "thiserror",
+ "reqwest 0.12.28",
+ "thiserror 2.0.18",
  "tokio",
  "tonic",
- "tonic-types",
+ "tracing",
 ]

 [[package]]
 name = "opentelemetry-proto"
-version = "0.32.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638"
+checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
 dependencies = [
  "opentelemetry",
  "opentelemetry_sdk",
@@ -3511,31 +3843,30 @@ dependencies = [

 [[package]]
 name = "opentelemetry_sdk"
-version = "0.32.1"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9"
+checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd"
 dependencies = [
  "futures-channel",
  "futures-executor",
  "futures-util",
  "opentelemetry",
  "percent-encoding",
- "portable-atomic",
- "rand 0.9.4",
- "thiserror",
+ "rand 0.9.2",
+ "thiserror 2.0.18",
  "tokio",
  "tokio-stream",
 ]

 [[package]]
 name = "os_info"
-version = "3.15.0"
+version = "3.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b"
+checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
 dependencies = [
  "android_system_properties",
  "log",
- "nix",
+ "nix 0.30.1",
  "objc2",
  "objc2-foundation",
  "objc2-ui-kit",
@@ -3591,6 +3922,12 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"

+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
 [[package]]
 name = "pear"
 version = "0.2.9"
@@ -3656,7 +3993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
 dependencies = [
  "phf_shared",
- "rand 0.8.6",
+ "rand 0.8.5",
 ]

 [[package]]
@@ -3670,18 +4007,18 @@ dependencies = [

 [[package]]
 name = "pin-project"
-version = "1.1.13"
+version = "1.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
 dependencies = [
  "pin-project-internal",
 ]

 [[package]]
 name = "pin-project-internal"
-version = "1.1.13"
+version = "1.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3694,6 +4031,12 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"

+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
 [[package]]
 name = "pkcs8"
 version = "0.10.2"
@@ -3706,9 +4049,9 @@ dependencies = [

 [[package]]
 name = "pkg-config"
-version = "0.3.33"
+version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"

 [[package]]
 name = "png"
@@ -3731,9 +4074,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"

 [[package]]
 name = "potential_utf"
-version = "0.1.5"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
 dependencies = [
  "zerovec",
 ]
@@ -3759,17 +4102,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"

-[[package]]
-name = "prefix-trie"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
-dependencies = [
- "either",
- "ipnet",
- "num-traits",
-]
-
 [[package]]
 name = "prettyplease"
 version = "0.2.37"
@@ -3786,7 +4118,7 @@ version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
 dependencies = [
- "toml_edit 0.25.12+spec-1.1.0",
+ "toml_edit 0.25.5+spec-1.1.0",
 ]

 [[package]]
@@ -3834,10 +4166,29 @@ dependencies = [
 ]

 [[package]]
-name = "prost"
-version = "0.14.4"
+name = "profiling"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -3845,9 +4196,9 @@ dependencies = [

 [[package]]
 name = "prost-derive"
-version = "0.14.4"
+version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
+checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
 dependencies = [
  "anyhow",
  "itertools 0.14.0",
@@ -3858,18 +4209,28 @@ dependencies = [

 [[package]]
 name = "prost-types"
-version = "0.14.4"
+version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
+checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
 dependencies = [
  "prost",
 ]

 [[package]]
-name = "pulldown-cmark"
-version = "0.13.4"
+name = "psm"
+version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
+checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
+dependencies = [
+ "ar_archive_writer",
+ "cc",
+]
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6"
 dependencies = [
  "bitflags",
  "memchr",
@@ -3885,9 +4246,18 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"

 [[package]]
 name = "pxfm"
-version = "0.1.29"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]

 [[package]]
 name = "quick-error"
@@ -3909,8 +4279,8 @@ dependencies = [
  "quinn-udp",
  "rustc-hash",
  "rustls",
- "socket2",
- "thiserror",
+ "socket2 0.6.3",
+ "thiserror 2.0.18",
  "tokio",
  "tracing",
  "web-time",
@@ -3926,13 +4296,13 @@ dependencies = [
  "bytes",
  "getrandom 0.3.4",
  "lru-slab",
- "rand 0.9.4",
+ "rand 0.9.2",
  "ring",
  "rustc-hash",
  "rustls",
  "rustls-pki-types",
  "slab",
- "thiserror",
+ "thiserror 2.0.18",
  "tinyvec",
  "tracing",
  "web-time",
@@ -3947,25 +4317,25 @@ dependencies = [
  "cfg_aliases",
  "libc",
  "once_cell",
- "socket2",
+ "socket2 0.6.3",
  "tracing",
  "windows-sys 0.60.2",
 ]

 [[package]]
 name = "quote"
-version = "1.0.46"
+version = "1.0.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
 dependencies = [
  "proc-macro2",
 ]

 [[package]]
 name = "quoted_printable"
-version = "0.5.2"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
+checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"

 [[package]]
 name = "r-efi"
@@ -3981,18 +4351,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"

 [[package]]
 name = "rand"
-version = "0.8.6"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "rand_core 0.6.4",
 ]

 [[package]]
 name = "rand"
-version = "0.9.4"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
 dependencies = [
  "rand_chacha",
  "rand_core 0.9.5",
@@ -4000,13 +4370,13 @@ dependencies = [

 [[package]]
 name = "rand"
-version = "0.10.1"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
 dependencies = [
  "chacha20",
  "getrandom 0.4.2",
- "rand_core 0.10.1",
+ "rand_core 0.10.0",
 ]

 [[package]]
@@ -4039,9 +4409,79 @@ dependencies = [

 [[package]]
 name = "rand_core"
-version = "0.10.1"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
+
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools 0.14.0",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.2",
+ "rand_chacha",
+ "simd_helpers",
+ "thiserror 2.0.18",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]

 [[package]]
 name = "recaptcha-verify"
@@ -4065,9 +4505,9 @@ dependencies = [

 [[package]]
 name = "regex"
-version = "1.12.4"
+version = "1.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -4088,52 +4528,15 @@ dependencies = [

 [[package]]
 name = "regex-syntax"
-version = "0.8.11"
+version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"

 [[package]]
 name = "reqwest"
 version = "0.12.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-rustls",
- "hyper-util",
- "js-sys",
- "log",
- "percent-encoding",
- "pin-project-lite",
- "rustls",
- "rustls-native-certs",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper",
- "tokio",
- "tokio-rustls",
- "tower",
- "tower-http 0.6.11",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "reqwest"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -4157,6 +4560,47 @@ dependencies = [
  "pin-project-lite",
  "quinn",
  "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
  "rustls-pki-types",
  "rustls-platform-verifier",
  "serde",
@@ -4164,14 +4608,12 @@ dependencies = [
  "sync_wrapper",
  "tokio",
  "tokio-rustls",
- "tokio-util",
  "tower",
- "tower-http 0.6.11",
+ "tower-http",
  "tower-service",
  "url",
  "wasm-bindgen",
  "wasm-bindgen-futures",
- "wasm-streams",
  "web-sys",
 ]

@@ -4181,6 +4623,25 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"

+[[package]]
+name = "resolvematrix"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfcc6f56a81348db1fc6591356cfea9dc840408c75553b2fe225f86de43274"
+dependencies = [
+ "hickory-resolver",
+ "reqwest 0.13.2",
+ "serde",
+ "thiserror 2.0.18",
+ "tracing",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+
 [[package]]
 name = "ring"
 version = "0.17.14"
@@ -4197,27 +4658,31 @@ dependencies = [

 [[package]]
 name = "ruma"
-version = "0.15.1"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.10.1"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "assign",
+ "continuwuity-admin-api",
+ "draupnir-antispam",
  "js_int",
  "js_option",
+ "meowlnir-antispam",
  "ruma-appservice-api",
  "ruma-client-api",
  "ruma-common",
  "ruma-events",
  "ruma-federation-api",
+ "ruma-identifiers-validation",
+ "ruma-identity-service-api",
  "ruma-push-gateway-api",
  "ruma-signatures",
- "ruma-state-res",
  "web-time",
 ]

 [[package]]
 name = "ruma-appservice-api"
-version = "0.15.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.10.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "js_int",
  "ruma-common",
@@ -4228,12 +4693,13 @@ dependencies = [

 [[package]]
 name = "ruma-client-api"
-version = "0.23.1"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.18.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "as_variant",
  "assign",
  "bytes",
+ "date_header",
  "http",
  "js_int",
  "js_option",
@@ -4243,69 +4709,72 @@ dependencies = [
  "serde",
  "serde_html_form",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "url",
  "web-time",
 ]

 [[package]]
 name = "ruma-common"
-version = "0.18.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.13.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "as_variant",
  "base64 0.22.1",
  "bytes",
- "date_header",
  "form_urlencoded",
- "getrandom 0.4.2",
+ "getrandom 0.2.17",
  "http",
  "indexmap",
  "js_int",
  "konst",
  "percent-encoding",
- "rand 0.10.1",
+ "rand 0.10.0",
  "regex",
  "ruma-identifiers-validation",
  "ruma-macros",
  "serde",
  "serde_html_form",
  "serde_json",
- "thiserror",
+ "smallvec",
+ "thiserror 2.0.18",
  "time",
  "tracing",
  "url",
  "uuid",
  "web-time",
  "wildmatch",
- "zeroize",
 ]

 [[package]]
 name = "ruma-events"
-version = "0.33.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.28.1"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "as_variant",
  "indexmap",
  "js_int",
  "js_option",
+ "percent-encoding",
  "pulldown-cmark",
+ "regex",
  "ruma-common",
+ "ruma-identifiers-validation",
  "ruma-macros",
  "serde",
  "serde_json",
- "thiserror",
+ "smallvec",
+ "thiserror 2.0.18",
  "tracing",
+ "url",
  "web-time",
  "wildmatch",
- "zeroize",
 ]

 [[package]]
 name = "ruma-federation-api"
-version = "0.14.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.9.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "bytes",
  "headers",
@@ -4315,31 +4784,39 @@ dependencies = [
  "js_int",
  "memchr",
  "mime",
- "rand 0.10.1",
+ "rand 0.10.0",
  "ruma-common",
  "ruma-events",
- "ruma-signatures",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "tracing",
 ]

 [[package]]
 name = "ruma-identifiers-validation"
-version = "0.12.1"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.9.5"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "js_int",
- "thiserror",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ruma-identity-service-api"
+version = "0.9.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
+dependencies = [
+ "js_int",
+ "ruma-common",
+ "serde",
 ]

 [[package]]
 name = "ruma-macros"
-version = "0.18.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.13.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
- "as_variant",
  "cfg-if",
  "proc-macro-crate",
  "proc-macro2",
@@ -4347,13 +4824,13 @@ dependencies = [
  "ruma-identifiers-validation",
  "serde",
  "syn",
- "toml 1.1.2+spec-1.1.0",
+ "toml 0.8.23",
 ]

 [[package]]
 name = "ruma-push-gateway-api"
-version = "0.14.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.9.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "js_int",
  "ruma-common",
@@ -4364,50 +4841,25 @@ dependencies = [

 [[package]]
 name = "ruma-signatures"
-version = "0.20.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
+version = "0.15.0"
+source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=1415caf8a32af4d943580c5ea4e12be1974593c2#1415caf8a32af4d943580c5ea4e12be1974593c2"
 dependencies = [
  "base64 0.22.1",
  "ed25519-dalek",
- "memchr",
  "pkcs8",
- "rand 0.10.1",
+ "rand 0.10.0",
+ "rand_core 0.6.4",
  "ruma-common",
  "serde_json",
- "sha2 0.10.9",
- "thiserror",
-]
-
-[[package]]
-name = "ruma-state-res"
-version = "0.16.0"
-source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
-dependencies = [
- "js_int",
- "ruma-common",
- "ruma-events",
- "ruma-signatures",
- "serde",
- "serde_json",
- "thiserror",
- "tracing",
-]
-
-[[package]]
-name = "ruminuwuity"
-version = "26.6.0-alpha.1"
-dependencies = [
- "assign",
- "ruma",
- "serde",
- "serde_json",
- "wildmatch",
+ "sha2",
+ "subslice",
+ "thiserror 2.0.18",
 ]

 [[package]]
 name = "rust-librocksdb-sys"
-version = "0.45.1+11.1.1"
-source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=0a25ff92f7c09b55eec496b9c192c7d5136ab2b8#0a25ff92f7c09b55eec496b9c192c7d5136ab2b8"
+version = "0.42.0+10.10.1"
+source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9#31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
 dependencies = [
  "bindgen",
  "bzip2-sys",
@@ -4423,8 +4875,8 @@ dependencies = [

 [[package]]
 name = "rust-rocksdb"
-version = "0.49.1"
-source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=0a25ff92f7c09b55eec496b9c192c7d5136ab2b8#0a25ff92f7c09b55eec496b9c192c7d5136ab2b8"
+version = "0.46.0"
+source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9#31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
 dependencies = [
  "libc",
  "parking_lot",
@@ -4439,9 +4891,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"

 [[package]]
 name = "rustc-hash"
-version = "2.1.2"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"

 [[package]]
 name = "rustc_version"
@@ -4452,6 +4904,15 @@ dependencies = [
  "semver",
 ]

+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom 7.1.3",
+]
+
 [[package]]
 name = "rustix"
 version = "1.1.4"
@@ -4467,9 +4928,9 @@ dependencies = [

 [[package]]
 name = "rustls"
-version = "0.23.40"
+version = "0.23.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
 dependencies = [
  "aws-lc-rs",
  "log",
@@ -4483,9 +4944,9 @@ dependencies = [

 [[package]]
 name = "rustls-native-certs"
-version = "0.8.4"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
 dependencies = [
  "openssl-probe",
  "rustls-pki-types",
@@ -4494,10 +4955,19 @@ dependencies = [
 ]

 [[package]]
-name = "rustls-pki-types"
-version = "1.14.1"
+name = "rustls-pemfile"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
 dependencies = [
  "web-time",
  "zeroize",
@@ -4505,11 +4975,11 @@ dependencies = [

 [[package]]
 name = "rustls-platform-verifier"
-version = "0.7.0"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
 dependencies = [
- "core-foundation 0.10.1",
+ "core-foundation",
  "core-foundation-sys",
  "jni",
  "log",
@@ -4532,9 +5002,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"

 [[package]]
 name = "rustls-webpki"
-version = "0.103.13"
+version = "0.103.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
 dependencies = [
  "aws-lc-rs",
  "ring",
@@ -4558,7 +5028,7 @@ dependencies = [
  "futures-util",
  "pin-project",
  "thingbuf",
- "thiserror",
+ "thiserror 2.0.18",
  "unicode-segmentation",
 ]

@@ -4586,6 +5056,17 @@ dependencies = [
  "regex",
 ]

+[[package]]
+name = "saphyr-parser-bw"
+version = "0.0.608"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55ae5ea09894b6d5382621db78f586df37ef18ab581bf32c754e75076b124b1"
+dependencies = [
+ "arraydeque",
+ "smallvec",
+ "thiserror 2.0.18",
+]
+
 [[package]]
 name = "schannel"
 version = "0.1.29"
@@ -4603,9 +5084,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"

 [[package]]
 name = "sd-notify"
-version = "0.5.0"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4ef7359e694bfaf1dd27a30f9d760b54c00dfae9f19bd0c05a39bc9128fe76"
+checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4"
 dependencies = [
  "libc",
 ]
@@ -4617,7 +5098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
 dependencies = [
  "bitflags",
- "core-foundation 0.10.1",
+ "core-foundation",
  "core-foundation-sys",
  "libc",
  "security-framework-sys",
@@ -4635,9 +5116,9 @@ dependencies = [

 [[package]]
 name = "semver"
-version = "1.0.28"
+version = "1.0.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
 dependencies = [
  "serde",
  "serde_core",
@@ -4645,13 +5126,14 @@ dependencies = [

 [[package]]
 name = "sentry"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "931a20b0da02350676e3d6d3c9028d58eaa448cf42a866712eec5845a505421e"
+checksum = "d92d893ba7469d361a6958522fa440e4e2bc8bf4c5803cd1bf40b9af63f8f9a8"
 dependencies = [
  "cfg_aliases",
  "httpdate",
- "reqwest 0.13.4",
+ "reqwest 0.12.28",
+ "rustls",
  "sentry-backtrace",
  "sentry-contexts",
  "sentry-core",
@@ -4661,13 +5143,14 @@ dependencies = [
  "sentry-tower",
  "sentry-tracing",
  "tokio",
+ "ureq",
 ]

 [[package]]
 name = "sentry-backtrace"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "911ee36abf5b7fa335fccd5f54361ba9c16baea5f0c3bb361a687b6c195c21cf"
+checksum = "5f8784d0a27b5cd4b5f75769ffc84f0b7580e3c35e1af9cd83cb90b612d769cc"
 dependencies = [
  "backtrace",
  "regex",
@@ -4676,9 +5159,9 @@ dependencies = [

 [[package]]
 name = "sentry-contexts"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b9d7d469e9e22741c17ca23fb8b42d79861590eb7cf330f3da34fc1e4bc1bc6"
+checksum = "0e5eb42f4cd4f9fdfec9e3b07b25a4c9769df83d218a7e846658984d5948ad3e"
 dependencies = [
  "hostname",
  "libc",
@@ -4690,11 +5173,11 @@ dependencies = [

 [[package]]
 name = "sentry-core"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "545dc562b6758d646ac19e1407f4ebc26d452111386743e03323464bc48bb2e0"
+checksum = "b0b1e7ca40f965db239da279bf278d87b7407469b98835f27f0c8e59ed189b06"
 dependencies = [
- "rand 0.9.4",
+ "rand 0.9.2",
  "sentry-types",
  "serde",
  "serde_json",
@@ -4703,9 +5186,9 @@ dependencies = [

 [[package]]
 name = "sentry-debug-images"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "660e9def38a573a869a182f7e90f58aaaa460f38b92b31fd1755ec537193bb48"
+checksum = "002561e49ea3a9de316e2efadc40fae553921b8ff41448f02ea85fd135a778d6"
 dependencies = [
  "findshlibs",
  "sentry-core",
@@ -4713,9 +5196,9 @@ dependencies = [

 [[package]]
 name = "sentry-log"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c13b9313bdd6a9db19e65ac0e4754e64dea6f18cdd15444656abb050db4538d"
+checksum = "e200860daf76e09f9ad111bce25928f96bedbb84bc5934b37f05bb445727c70e"
 dependencies = [
  "bitflags",
  "log",
@@ -4724,9 +5207,9 @@ dependencies = [

 [[package]]
 name = "sentry-panic"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "772d9de150c8ca910c835353c85f434457348fdd21208f9b3da3574202b1dc5d"
+checksum = "8906f8be87aea5ac7ef937323fb655d66607427f61007b99b7cb3504dc5a156c"
 dependencies = [
  "sentry-backtrace",
  "sentry-core",
@@ -4734,9 +5217,9 @@ dependencies = [

 [[package]]
 name = "sentry-tower"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2abea154597936d5df2d39fbe8aac16d584de6b3572c70c39558764d9d2efe15"
+checksum = "56aebe376310840b49dad4cca55c7b32d9abdc14946cd071d4158ecb149b63a4"
 dependencies = [
  "http",
  "pin-project",
@@ -4748,9 +5231,9 @@ dependencies = [

 [[package]]
 name = "sentry-tracing"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c51ec9620a4d398dcdf7ee90effbf8d8691cfa24e91978bfa8565cac039d4980"
+checksum = "5b07eefe04486316c57aba08ab53dd44753c25102d1d3fe05775cc93a13262d9"
 dependencies = [
  "bitflags",
  "sentry-backtrace",
@@ -4761,16 +5244,16 @@ dependencies = [

 [[package]]
 name = "sentry-types"
-version = "0.48.2"
+version = "0.46.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "041359745a44dd2e14fe21b7510fe7ca8b5beffce6636a0b52e5bc7d5f736887"
+checksum = "567711f01f86a842057e1fc17779eba33a336004227e1a1e7e6cc2599e22e259"
 dependencies = [
  "debugid",
  "hex",
- "rand 0.9.4",
+ "rand 0.9.2",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "time",
  "url",
  "uuid",
@@ -4788,18 +5271,19 @@ dependencies = [

 [[package]]
 name = "serde-saphyr"
-version = "0.0.27"
+version = "0.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5897b4c3faadadd35fdb6689f015641f3bc481d5adaaac56231ea15aeb243db3"
+checksum = "4a6fc4aa0da972ba0f51cf5c1bb16e9dba35334adc6831b09b3ffb0ec20bb264"
 dependencies = [
  "ahash",
  "annotate-snippets",
  "base64 0.22.1",
  "encoding_rs_io",
  "getrandom 0.3.4",
- "granit-parser",
  "nohash-hasher",
  "num-traits",
+ "regex",
+ "saphyr-parser-bw",
  "serde",
  "smallvec",
  "zmij",
@@ -4827,9 +5311,9 @@ dependencies = [

 [[package]]
 name = "serde_html_form"
-version = "0.4.0"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f"
+checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
 dependencies = [
  "form_urlencoded",
  "indexmap",
@@ -4840,9 +5324,9 @@ dependencies = [

 [[package]]
 name = "serde_json"
-version = "1.0.150"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
 dependencies = [
  "itoa",
  "memchr",
@@ -4864,9 +5348,9 @@ dependencies = [

 [[package]]
 name = "serde_regex"
-version = "1.2.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bafc8d0c5330cecff10f16b459b479fd9acaa5b4acd7167301414e21b0057012"
+checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
 dependencies = [
  "regex",
  "serde",
@@ -4883,9 +5367,9 @@ dependencies = [

 [[package]]
 name = "serde_spanned"
-version = "1.1.1"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
 dependencies = [
  "serde_core",
 ]
@@ -4910,18 +5394,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
- "digest 0.10.7",
-]
-
-[[package]]
-name = "sha1"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "digest 0.11.3",
+ "digest",
 ]

 [[package]]
@@ -4932,18 +5405,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
- "digest 0.10.7",
-]
-
-[[package]]
-name = "sha2"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "digest 0.11.3",
+ "digest",
 ]

 [[package]]
@@ -4955,7 +5417,7 @@ dependencies = [
  "async-trait",
  "bytes",
  "hex",
- "sha2 0.10.9",
+ "sha2",
  "tokio",
 ]

@@ -4974,12 +5436,6 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"

-[[package]]
-name = "shlex"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
-
 [[package]]
 name = "signal-hook"
 version = "0.3.18"
@@ -5022,31 +5478,24 @@ dependencies = [

 [[package]]
 name = "simd-adler32"
-version = "0.3.9"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"

 [[package]]
-name = "simd_cesu8"
-version = "1.1.1"
+name = "simd_helpers"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
 dependencies = [
- "rustc_version",
- "simdutf8",
+ "quote",
 ]

-[[package]]
-name = "simdutf8"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
-
 [[package]]
 name = "siphasher"
-version = "1.0.3"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"

 [[package]]
 name = "slab"
@@ -5066,18 +5515,28 @@ dependencies = [

 [[package]]
 name = "smallvec"
-version = "1.15.2"
+version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 dependencies = [
  "serde",
 ]

 [[package]]
 name = "socket2"
-version = "0.6.4"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
 dependencies = [
  "libc",
  "windows-sys 0.61.2",
@@ -5108,6 +5567,19 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"

+[[package]]
+name = "stacker"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "strict"
 version = "0.2.0"
@@ -5145,6 +5617,15 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"

+[[package]]
+name = "subslice"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a8e4809a3bb02de01f1f7faf1ba01a83af9e8eabcd4d31dd6e413d14d56aae"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "subtle"
 version = "2.6.1"
@@ -5153,9 +5634,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"

 [[package]]
 name = "syn"
-version = "2.0.118"
+version = "2.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5182,27 +5663,6 @@ dependencies = [
  "syn",
 ]

-[[package]]
-name = "system-configuration"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
-dependencies = [
- "bitflags",
- "core-foundation 0.9.4",
- "system-configuration-sys",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
 [[package]]
 name = "tagptr"
 version = "0.2.0"
@@ -5232,7 +5692,7 @@ dependencies = [
  "lazy-regex",
  "minimad",
  "serde",
- "thiserror",
+ "thiserror 2.0.18",
  "unicode-width 0.1.14",
 ]

@@ -5246,13 +5706,33 @@ dependencies = [
  "pin-project",
 ]

+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
 [[package]]
 name = "thiserror"
 version = "2.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]

 [[package]]
@@ -5275,6 +5755,20 @@ dependencies = [
  "cfg-if",
 ]

+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
+]
+
 [[package]]
 name = "tikv-jemalloc-ctl"
 version = "0.6.1"
@@ -5305,11 +5799,12 @@ dependencies = [

 [[package]]
 name = "time"
-version = "0.3.49"
+version = "0.3.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
 dependencies = [
  "deranged",
+ "itoa",
  "num-conv",
  "powerfmt",
  "serde_core",
@@ -5319,15 +5814,15 @@ dependencies = [

 [[package]]
 name = "time-core"
-version = "0.1.9"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"

 [[package]]
 name = "time-macros"
-version = "0.2.29"
+version = "0.2.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
 dependencies = [
  "num-conv",
  "time-core",
@@ -5335,9 +5830,9 @@ dependencies = [

 [[package]]
 name = "tinystr"
-version = "0.8.3"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
 dependencies = [
  "displaydoc",
  "zerovec",
@@ -5360,16 +5855,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"

 [[package]]
 name = "tokio"
-version = "1.52.3"
+version = "1.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
 dependencies = [
  "bytes",
  "libc",
  "mio",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2",
+ "socket2 0.6.3",
  "tokio-macros",
  "tracing",
  "windows-sys 0.61.2",
@@ -5377,9 +5872,9 @@ dependencies = [

 [[package]]
 name = "tokio-macros"
-version = "2.7.0"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5388,9 +5883,9 @@ dependencies = [

 [[package]]
 name = "tokio-metrics"
-version = "0.5.0"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9e81d53caf955549b1dec7af4ac2149e94cc25ed97b4a545151140281e2f528"
+checksum = "0e0410015c6db7b67b9c9ab2a3af4d74a942d637ff248d0d055073750deac6f9"
 dependencies = [
  "futures-util",
  "pin-project-lite",
@@ -5452,26 +5947,13 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
 dependencies = [
  "indexmap",
  "serde_core",
- "serde_spanned 1.1.1",
+ "serde_spanned 1.0.4",
  "toml_datetime 0.7.5+spec-1.1.0",
  "toml_parser",
  "toml_writer",
  "winnow 0.7.15",
 ]

-[[package]]
-name = "toml"
-version = "1.1.2+spec-1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
-dependencies = [
- "serde_core",
- "serde_spanned 1.1.1",
- "toml_datetime 1.1.1+spec-1.1.0",
- "toml_parser",
- "winnow 1.0.3",
-]
-
 [[package]]
 name = "toml_datetime"
 version = "0.6.11"
@@ -5492,9 +5974,9 @@ dependencies = [

 [[package]]
 name = "toml_datetime"
-version = "1.1.1+spec-1.1.0"
+version = "1.0.1+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
 dependencies = [
  "serde_core",
 ]
@@ -5515,23 +5997,23 @@ dependencies = [

 [[package]]
 name = "toml_edit"
-version = "0.25.12+spec-1.1.0"
+version = "0.25.5+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
 dependencies = [
  "indexmap",
- "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_datetime 1.0.1+spec-1.1.0",
  "toml_parser",
- "winnow 1.0.3",
+ "winnow 1.0.0",
 ]

 [[package]]
 name = "toml_parser"
-version = "1.1.2+spec-1.1.0"
+version = "1.0.10+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
 dependencies = [
- "winnow 1.0.3",
+ "winnow 1.0.0",
 ]

 [[package]]
@@ -5542,15 +6024,15 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"

 [[package]]
 name = "toml_writer"
-version = "1.1.1+spec-1.1.0"
+version = "1.0.7+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"

 [[package]]
 name = "tonic"
-version = "0.14.6"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
+checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
 dependencies = [
  "async-trait",
  "axum",
@@ -5565,7 +6047,7 @@ dependencies = [
  "hyper-util",
  "percent-encoding",
  "pin-project",
- "socket2",
+ "socket2 0.6.3",
  "sync_wrapper",
  "tokio",
  "tokio-stream",
@@ -5577,26 +6059,15 @@ dependencies = [

 [[package]]
 name = "tonic-prost"
-version = "0.14.6"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
+checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
 dependencies = [
  "bytes",
  "prost",
  "tonic",
 ]

-[[package]]
-name = "tonic-types"
-version = "0.14.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8"
-dependencies = [
- "prost",
- "prost-types",
- "tonic",
-]
-
 [[package]]
 name = "tower"
 version = "0.5.3"
@@ -5618,9 +6089,9 @@ dependencies = [

 [[package]]
 name = "tower-http"
-version = "0.6.11"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
 dependencies = [
  "async-compression",
  "bitflags",
@@ -5630,30 +6101,7 @@ dependencies = [
  "http",
  "http-body",
  "http-body-util",
- "pin-project-lite",
- "tokio",
- "tokio-util",
- "tower",
- "tower-layer",
- "tower-service",
- "url",
-]
-
-[[package]]
-name = "tower-http"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
-dependencies = [
- "async-compression",
- "bitflags",
- "bytes",
- "futures-core",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "percent-encoding",
+ "iri-string",
  "pin-project-lite",
  "tokio",
  "tokio-util",
@@ -5755,9 +6203,9 @@ dependencies = [

 [[package]]
 name = "tracing-opentelemetry"
-version = "0.33.0"
+version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26"
+checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
 dependencies = [
  "js-sys",
  "opentelemetry",
@@ -5795,15 +6243,24 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"

 [[package]]
 name = "typenum"
-version = "1.20.1"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"

 [[package]]
 name = "typewit"
-version = "1.15.2"
+version = "1.14.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"
+checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71"
+dependencies = [
+ "typewit_proc_macros",
+]
+
+[[package]]
+name = "typewit_proc_macros"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"

 [[package]]
 name = "uname"
@@ -5837,9 +6294,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

 [[package]]
 name = "unicode-segmentation"
-version = "1.13.3"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"

 [[package]]
 name = "unicode-width"
@@ -5865,6 +6322,34 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"

+[[package]]
+name = "ureq"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc"
+dependencies = [
+ "base64 0.22.1",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf-8",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
+dependencies = [
+ "base64 0.22.1",
+ "http",
+ "httparse",
+ "log",
+]
+
 [[package]]
 name = "url"
 version = "2.5.8"
@@ -5898,9 +6383,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"

 [[package]]
 name = "uuid"
-version = "1.23.3"
+version = "1.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
 dependencies = [
  "getrandom 0.4.2",
  "js-sys",
@@ -5908,6 +6393,17 @@ dependencies = [
  "wasm-bindgen",
 ]

+[[package]]
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "validator"
 version = "0.20.0"
@@ -5983,11 +6479,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"

 [[package]]
 name = "wasip2"
-version = "1.0.4+wasi-0.2.12"
+version = "1.0.2+wasi-0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
 dependencies = [
- "wit-bindgen 0.57.1",
+ "wit-bindgen",
 ]

 [[package]]
@@ -5996,14 +6492,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
 dependencies = [
- "wit-bindgen 0.51.0",
+ "wit-bindgen",
 ]

 [[package]]
 name = "wasm-bindgen"
-version = "0.2.125"
+version = "0.2.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -6014,19 +6510,23 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.75"
+version = "0.4.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
+checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
 dependencies = [
+ "cfg-if",
+ "futures-util",
  "js-sys",
+ "once_cell",
  "wasm-bindgen",
+ "web-sys",
 ]

 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.125"
+version = "0.2.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -6034,9 +6534,9 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.125"
+version = "0.2.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
 dependencies = [
  "bumpalo",
  "proc-macro2",
@@ -6047,9 +6547,9 @@ dependencies = [

 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.125"
+version = "0.2.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
 dependencies = [
  "unicode-ident",
 ]
@@ -6078,9 +6578,9 @@ dependencies = [

 [[package]]
 name = "wasm-streams"
-version = "0.5.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
 dependencies = [
  "futures-util",
  "js-sys",
@@ -6103,9 +6603,9 @@ dependencies = [

 [[package]]
 name = "web-sys"
-version = "0.3.102"
+version = "0.3.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6135,9 +6635,18 @@ dependencies = [

 [[package]]
 name = "webpki-root-certs"
-version = "1.0.7"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
+checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
 dependencies = [
  "rustls-pki-types",
 ]
@@ -6198,32 +6707,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"

 [[package]]
-name = "windows-registry"
-version = "0.6.1"
+name = "windows-sys"
+version = "0.45.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
 dependencies = [
- "windows-link",
- "windows-result",
- "windows-strings",
+ "windows-targets 0.42.2",
 ]

 [[package]]
-name = "windows-result"
-version = "0.4.1"
+name = "windows-sys"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
-dependencies = [
- "windows-link",
+ "windows-targets 0.48.5",
 ]

 [[package]]
@@ -6235,6 +6733,15 @@ dependencies = [
  "windows-targets 0.52.6",
 ]

+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.60.2"
@@ -6253,6 +6760,36 @@ dependencies = [
  "windows-link",
 ]

+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.52.6"
@@ -6286,6 +6823,18 @@ dependencies = [
  "windows_x86_64_msvc 0.53.1",
 ]

+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
@@ -6298,6 +6847,18 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"

+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
@@ -6310,6 +6871,18 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"

+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
@@ -6334,6 +6907,18 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"

+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
@@ -6346,6 +6931,18 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"

+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
@@ -6358,6 +6955,18 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"

+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
@@ -6370,6 +6979,18 @@ version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"

+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"
@@ -6393,13 +7014,23 @@ dependencies = [

 [[package]]
 name = "winnow"
-version = "1.0.3"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
 dependencies = [
  "memchr",
 ]

+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "wit-bindgen"
 version = "0.51.0"
@@ -6409,12 +7040,6 @@ dependencies = [
  "wit-bindgen-rust-macro",
 ]

-[[package]]
-name = "wit-bindgen"
-version = "0.57.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-
 [[package]]
 name = "wit-bindgen-core"
 version = "0.51.0"
@@ -6496,9 +7121,26 @@ dependencies = [

 [[package]]
 name = "writeable"
-version = "0.6.3"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "x509-parser"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom 7.1.3",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]

 [[package]]
 name = "xml5ever"
@@ -6513,7 +7155,7 @@ dependencies = [

 [[package]]
 name = "xtask"
-version = "26.6.0-alpha.1"
+version = "0.5.7-alpha.1"
 dependencies = [
  "askama",
  "cargo_metadata",
@@ -6522,6 +7164,12 @@ dependencies = [
  "conduwuit_admin",
 ]

+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
 [[package]]
 name = "yansi"
 version = "1.0.1"
@@ -6530,9 +7178,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

 [[package]]
 name = "yoke"
-version = "0.8.3"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
 dependencies = [
  "stable_deref_trait",
  "yoke-derive",
@@ -6541,9 +7189,9 @@ dependencies = [

 [[package]]
 name = "yoke-derive"
-version = "0.8.2"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6553,18 +7201,18 @@ dependencies = [

 [[package]]
 name = "zerocopy"
-version = "0.8.52"
+version = "0.8.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
 dependencies = [
  "zerocopy-derive",
 ]

 [[package]]
 name = "zerocopy-derive"
-version = "0.8.52"
+version = "0.8.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6573,18 +7221,18 @@ dependencies = [

 [[package]]
 name = "zerofrom"
-version = "0.1.8"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
 dependencies = [
  "zerofrom-derive",
 ]

 [[package]]
 name = "zerofrom-derive"
-version = "0.1.7"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6594,15 +7242,15 @@ dependencies = [

 [[package]]
 name = "zeroize"
-version = "1.9.0"
+version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"

 [[package]]
 name = "zerotrie"
-version = "0.2.4"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
 dependencies = [
  "displaydoc",
  "yoke",
@@ -6611,9 +7259,9 @@ dependencies = [

 [[package]]
 name = "zerovec"
-version = "0.11.6"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
 dependencies = [
  "yoke",
  "zerofrom",
@@ -6622,9 +7270,9 @@ dependencies = [

 [[package]]
 name = "zerovec-derive"
-version = "0.11.3"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6672,10 +7320,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"

 [[package]]
-name = "zune-jpeg"
-version = "0.5.15"
+name = "zune-inflate"
+version = "0.2.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
 dependencies = [
  "zune-core",
 ]
diff --git c/Cargo.toml i/Cargo.toml
index 519de9d2d..131aa6bf6 100644
--- c/Cargo.toml
+++ i/Cargo.toml
@@ -559,6 +559,9 @@ features = ["std"]
 [workspace.dependencies.nonzero_ext]
 version = "0.3.0"

+[workspace.dependencies.resolvematrix]
+version = "0.0.3"
+
 #
 # Patches
 #
diff --git c/src/admin/Cargo.toml i/src/admin/Cargo.toml
index 4afed12b4..1f82978a4 100644
--- c/src/admin/Cargo.toml
+++ i/src/admin/Cargo.toml
@@ -92,6 +92,7 @@ serde-saphyr.workspace = true
 tokio.workspace = true
 tracing-subscriber.workspace = true
 tracing.workspace = true
+resolvematrix.workspace = true

 [lints]
 workspace = true
diff --git c/src/admin/debug/commands.rs i/src/admin/debug/commands.rs
index 1aa9cb062..b6bf16ae0 100644
--- c/src/admin/debug/commands.rs
+++ i/src/admin/debug/commands.rs
@@ -20,6 +20,7 @@
 };
 use futures::{FutureExt, StreamExt, TryStreamExt};
 use lettre::message::Mailbox;
+use resolvematrix::server::{MatrixResolver, ResolvedDestination};
 use ruma::{
 	CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
 	OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, UInt,
@@ -1007,13 +1008,20 @@ pub(super) async fn resolve_true_destination(
 		);
 	}

-	let actual = self
-		.services
-		.resolver
-		.resolve_actual_dest(&server_name, !no_cache)
-		.await?;
+	let resolver: &MatrixResolver = if no_cache {
+		&MatrixResolver::new()?
+	} else {
+		&self.services.resolver.resolver
+	};

-	let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host);
+	let actual = resolver.resolve_server(&server_name.as_str()).await?;
+
+	let destination = match actual.destination {
+		| ResolvedDestination::Literal(addr) => addr.to_string(),
+		| ResolvedDestination::Named(host, port) => format!("{host}:{port}"),
+	};
+
+	let msg = format!("Destination: {}\nHostname URI (SNI): {}", destination, actual.host);
 	self.write_str(&msg).await
 }

diff --git c/src/admin/query/resolver.rs i/src/admin/query/resolver.rs
index d651e5bff..e2cb5eca3 100644
--- c/src/admin/query/resolver.rs
+++ i/src/admin/query/resolver.rs
@@ -46,7 +46,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
 	writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
 	writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;

-	let mut destinations = self.services.resolver.cache.destinations().boxed();
+	let mut destinations = self.services.resolver.dns.cache.destinations().boxed();

 	while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
 		if let Some(server_name) = server_name.as_ref() {
@@ -70,7 +70,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
 	writeln!(self, "| Server Name | IP  | Port | Expires | Overriding |").await?;
 	writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;

-	let mut overrides = self.services.resolver.cache.overrides().boxed();
+	let mut overrides = self.services.resolver.dns.cache.overrides().boxed();

 	while let Some((name, CachedOverride { ips, port, expire, overriding })) =
 		overrides.next().await
@@ -92,11 +92,11 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
 #[admin_command]
 async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
 	if all {
-		self.services.resolver.cache.clear().await;
+		self.services.resolver.dns.cache.clear().await;
 		writeln!(self, "Resolver caches cleared!").await
 	} else if let Some(name) = name {
-		self.services.resolver.cache.del_destination(&name);
-		self.services.resolver.cache.del_override(&name);
+		self.services.resolver.dns.cache.del_destination(&name);
+		self.services.resolver.dns.cache.del_override(&name);
 		self.write_str(&format!("Cleared {name} from resolver caches!"))
 			.await
 	} else {
diff --git c/src/core/Cargo.toml i/src/core/Cargo.toml
index 093b50f71..d99184b9d 100644
--- c/src/core/Cargo.toml
+++ i/src/core/Cargo.toml
@@ -117,6 +117,7 @@ url.workspace = true
 parking_lot.workspace = true
 lock_api.workspace = true
 hyper-util.workspace = true
+resolvematrix.workspace = true

 [target.'cfg(unix)'.dependencies]
 nix.workspace = true
diff --git c/src/core/error/mod.rs i/src/core/error/mod.rs
index 703726043..af7520c02 100644
--- c/src/core/error/mod.rs
+++ i/src/core/error/mod.rs
@@ -87,6 +87,8 @@ pub enum Error {
 	YamlDe(#[from] serde_saphyr::Error),
 	#[error(transparent)]
 	YamlSer(#[from] serde_saphyr::ser_error::Error),
+	#[error(transparent)]
+	ResolveServer(#[from] resolvematrix::server::ResolveServerError),

 	// ruma/conduwuit
 	#[error("Arithmetic operation failed: {0}")]
diff --git c/src/service/Cargo.toml i/src/service/Cargo.toml
index a0568db0b..0f63bd8ed 100644
--- c/src/service/Cargo.toml
+++ i/src/service/Cargo.toml
@@ -119,6 +119,7 @@ recaptcha-verify = { version = "0.2.0", default-features = false }
 reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"]  } # As long as recaptcha-verify's reqwest is outdated
 yansi.workspace = true
 lettre.workspace = true
+resolvematrix.workspace = true

 [target.'cfg(all(unix, target_os = "linux"))'.dependencies]
 sd-notify.workspace = true
diff --git c/src/service/client/mod.rs i/src/service/client/mod.rs
index 4792bd7ad..b17aab9ab 100644
--- c/src/service/client/mod.rs
+++ i/src/service/client/mod.rs
@@ -43,7 +43,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {

 		Ok(Arc::new(Self {
 			default: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.build()?,

 			url_preview: base(config)
@@ -51,19 +51,19 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 					builder_interface(builder, url_preview_bind_iface.as_deref())
 				})?
 				.local_address(url_preview_bind_addr)
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.timeout(Duration::from_secs(config.url_preview_timeout))
 				.redirect(redirect::Policy::limited(3))
 				.user_agent(url_preview_user_agent)
 				.build()?,

 			extern_media: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.redirect(redirect::Policy::limited(3))
 				.build()?,

 			well_known: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.connect_timeout(Duration::from_secs(config.well_known_conn_timeout))
 				.read_timeout(Duration::from_secs(config.well_known_timeout))
 				.timeout(Duration::from_secs(config.well_known_timeout))
@@ -72,7 +72,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			federation: base(config)?
-				.dns_resolver(resolver.resolver.hooked.clone())
+				.dns_resolver(resolver.dns.resolver.hooked.clone())
 				.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
 				.read_timeout(Duration::from_secs(config.federation_timeout))
 				.timeout(Duration::from_secs(
@@ -86,7 +86,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			synapse: base(config)?
-				.dns_resolver(resolver.resolver.hooked.clone())
+				.dns_resolver(resolver.dns.resolver.hooked.clone())
 				.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
 				.read_timeout(Duration::from_secs(config.federation_timeout.saturating_mul(6)))
 				.timeout(Duration::from_secs(
@@ -100,7 +100,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			sender: base(config)?
-				.dns_resolver(resolver.resolver.hooked.clone())
+				.dns_resolver(resolver.dns.resolver.hooked.clone())
 				.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
 				.read_timeout(Duration::from_secs(config.sender_timeout))
 				.timeout(Duration::from_secs(config.sender_timeout))
@@ -110,7 +110,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			appservice: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.connect_timeout(Duration::from_secs(5))
 				.read_timeout(Duration::from_secs(config.appservice_timeout))
 				.timeout(Duration::from_secs(config.appservice_timeout))
@@ -120,7 +120,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			pusher: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
 				.timeout(Duration::from_secs(config.pusher_timeout))
 				.pool_max_idle_per_host(1)
diff --git c/src/service/federation/execute.rs i/src/service/federation/execute.rs
index 0c147c1ef..4318bb016 100644
--- c/src/service/federation/execute.rs
+++ i/src/service/federation/execute.rs
@@ -7,6 +7,7 @@
 };
 use ipaddress::IPAddress;
 use reqwest::{Client, Method, Request, Response, Url};
+use resolvematrix::server::Resolution;
 use ruma::{
 	ServerName,
 	api::{
@@ -132,7 +133,7 @@ pub async fn execute_on<'i, T, PathBuilderInput>(
 async fn perform<T>(
 	&self,
 	dest: &ServerName,
-	actual: &ActualDest,
+	actual: &Resolution,
 	request: Request,
 	client: &Client,
 ) -> Result<T::IncomingResponse>
@@ -168,7 +169,7 @@ fn validate_url(&self, url: &Url) -> Result<()> {
 async fn handle_response<T>(
 	&self,
 	dest: &ServerName,
-	actual: &ActualDest,
+	actual: &Resolution,
 	method: &Method,
 	url: &Url,
 	response: Response,
@@ -199,7 +200,7 @@ async fn handle_response<T>(

 async fn into_http_response(
 	dest: &ServerName,
-	actual: &ActualDest,
+	actual: &Resolution,
 	method: &Method,
 	url: &Url,
 	mut response: Response,
@@ -211,7 +212,7 @@ async fn into_http_response(
 		request_url = %url,
 		response_url = %response.url(),
 		"Received response from {}",
-		actual.string(),
+		actual.base_url(),
 	);

 	let mut http_response_builder = http::Response::builder()
@@ -248,7 +249,7 @@ async fn into_http_response(
 }

 fn handle_error(
-	actual: &ActualDest,
+	actual: &Resolution,
 	method: &Method,
 	url: &Url,
 	mut e: reqwest::Error,
diff --git c/src/service/resolver/actual.rs i/src/service/resolver/actual.rs
index 1ca3b595c..e69de29bb 100644
--- c/src/service/resolver/actual.rs
+++ i/src/service/resolver/actual.rs
@@ -1,441 +0,0 @@
-use std::fmt::Debug;
-
-use conduwuit::{Err, Result, debug, debug_info, err, error, trace};
-use futures::{FutureExt, TryFutureExt};
-use hickory_resolver::{
-	net::{DnsError, NetError},
-	proto::rr::rdata::SRV,
-};
-use ipaddress::IPAddress;
-use ruma::ServerName;
-
-use super::{
-	cache::{CachedDest, CachedOverride, MAX_IPS},
-	fed::{FedDest, PortString, add_port_to_hostname, ensure_host_has_port, get_ip_with_port},
-};
-
-const DEFAULT_PORT: u16 = 8448;
-
-#[derive(Clone, Debug)]
-pub(crate) struct ActualDest {
-	pub(crate) dest: FedDest,
-	pub(crate) host: String,
-}
-
-impl ActualDest {
-	#[inline]
-	pub(crate) fn string(&self) -> String { self.dest.https_string() }
-}
-
-impl super::Service {
-	#[tracing::instrument(skip_all, level = "debug", name = "resolve")]
-	pub(crate) async fn get_actual_dest(&self, server_name: &ServerName) -> Result<ActualDest> {
-		let (CachedDest { dest, host, .. }, _cached) =
-			self.lookup_actual_dest(server_name).await?;
-
-		Ok(ActualDest { dest, host })
-	}
-
-	pub(crate) async fn lookup_actual_dest(
-		&self,
-		server_name: &ServerName,
-	) -> Result<(CachedDest, bool)> {
-		if let Ok(result) = self.cache.get_destination(server_name).await {
-			return Ok((result, true));
-		}
-
-		let _dedup = self.resolving.lock(server_name.as_str());
-		if let Ok(result) = self.cache.get_destination(server_name).await {
-			return Ok((result, true));
-		}
-
-		self.resolve_actual_dest(server_name, true)
-			.inspect_ok(|result| self.cache.set_destination(server_name, result))
-			.map_ok(|result| (result, false))
-			.boxed()
-			.await
-	}
-
-	/// Returns: `actual_destination` + `Host` http header
-	#[tracing::instrument(name = "actual", level = "debug", skip(self, cache))]
-	pub async fn resolve_actual_dest(
-		&self,
-		dest: &ServerName,
-		cache: bool,
-	) -> Result<CachedDest> {
-		debug!(
-			dest = %dest,
-			cache = %cache,
-			"Resolving server name and port"
-		);
-		// Ensure dest is a valid connection endpoint
-		self.validate_dest(dest)?;
-
-		// Clippy believes this can be a clone, however we are actually converting
-		// ServerName to String
-		#[allow(clippy::implicit_clone)]
-		let mut host_header = dest.to_string().to_owned();
-		let actual_dest = self
-			.resolve_server_name(dest, cache, &mut host_header)
-			.await?;
-
-		host_header = ensure_host_has_port(&host_header).to_string();
-
-		debug!(
-			dest = %dest,
-			actual_dest = %actual_dest,
-			host = %host_header,
-			"Finished resolving server name"
-		);
-		Ok(CachedDest {
-			dest: actual_dest,
-			host: host_header,
-			expire: CachedDest::default_expire(),
-		})
-	}
-
-	/// Performs the server resolution steps as per the specification:
-	/// <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
-	async fn resolve_server_name(
-		&self,
-		dest: &ServerName,
-		cache: bool,
-		host: &mut String,
-	) -> Result<FedDest> {
-		// 1. If `dest` is an IP, use it directly. If a port is provided as well
-		//    (IP:port socket pair) use that, otherwise default to port 8448
-		if let Some(fed_dest) = get_ip_with_port(dest.as_str()) {
-			debug!("1: IP literal with provided or default port");
-			return Ok(fed_dest);
-		}
-
-		// 2. If `dest` is a hostname and has a provided port (format of `host:port`),
-		//    resolve the hostname to an IP address and connect it and the provided port
-		if dest.as_str().contains(':') {
-			return self.resolve_2_host_port(dest, cache).await;
-		}
-
-		// Pre-resolve IP? Unsure what overrides exactly do, system is due to be removed
-		// either way https://matrix.to/#/!da26JtAjE6APGLnX8ncWsvc-skF2KQZ9Nw_MbNpYD2k/%24_hq6JP0JXANbMTMPdV64iZbgbsZdhy92M5ndDYGy6No
-		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, true)
-			.await?;
-
-		// Ensure server is running (not shutting down) before continuing resolution
-		self.services.server.check_running()?;
-
-		// 3. If `dest` is a hostname with no port, send GET to `https://<dest>/.well-known/matrix/server`.
-		//    If invalid JSON (throws error), skip to step 4. Otherwise, parse
-		//    `delegated` as `<hostname>[:<port>]` and...
-		if let Some(delegated) = self.request_well_known(dest.as_str()).await? {
-			return self.resolve_3_well_known(host, cache, delegated).await;
-		}
-
-		// 4. if .well-known errored, perform SRV (see 3.3)
-		if let Some(overrider) = self.query_srv_record(dest.as_str()).await? {
-			return self.resolve_4_srv_lookup(host, cache, overrider).await;
-		}
-
-		// 5. if .well-known errored and no SRV exists, resolve IP and connect on
-		//    default port (8448)
-		self.resolve_5_direct(dest, cache).await
-	}
-
-	/// Parse a host:port socket pair into separate parts, and resolve the
-	/// hostname into an IP address
-	async fn resolve_2_host_port(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
-		debug!("2: Hostname with included port");
-		let (host, port) = dest.as_str().split_once(':').unwrap();
-
-		self.conditional_query_and_cache(
-			host,
-			port.parse::<u16>().unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		Ok(FedDest::Named(
-			host.to_owned(),
-			port.try_into().unwrap_or_else(|_| FedDest::default_port()),
-		))
-	}
-
-	async fn resolve_3_well_known(
-		&self,
-		host: &mut String,
-		cache: bool,
-		delegated: String,
-	) -> Result<FedDest> {
-		debug!("3: A .well-known file is available");
-		*host = add_port_to_hostname(&delegated).uri_string();
-
-		// 3.1 - If <delegated> is IP:port, connect to that,
-		//       or IP with default port if no port provided (8448)
-		if let Some(host_and_port) = get_ip_with_port(&delegated) {
-			debug!("3.1: IP with port in .well-known file");
-			return Ok(host_and_port);
-		}
-
-		// 3.2 - If <delegated> is hostname:port, lookup IP for hostname and connect
-		if delegated.contains(':') {
-			return self.resolve_3_2_hostname_port(cache, &delegated).await;
-		}
-
-		// 3.3 - If <delegated> is not an IP and there is no port, lookup SRV
-		// `_matrix._tcp.<delegated>` (which may provide a new hostname + port to use,
-		// see steps 3.1 and 3.2)
-		trace!("Delegated hostname has no port, querying SRV");
-		if let Some(overrider) = self.query_srv_record(&delegated).await? {
-			return self.resolve_3_3_use_srv(cache, &delegated, overrider).await;
-		}
-
-		self.resolve_3_4_use_default_port(cache, delegated).await
-	}
-
-	async fn resolve_3_2_hostname_port(&self, cache: bool, delegated: &str) -> Result<FedDest> {
-		debug!("3.2: Hostname with port in .well-known file");
-		let (host, port) = &delegated.split_once(':').unwrap();
-		self.conditional_query_and_cache(
-			host,
-			port.parse::<u16>().unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		trace!("Successfully resolved IP for {delegated}");
-		Ok(FedDest::Named(
-			host.to_owned().to_owned(),
-			port.to_owned()
-				.try_into()
-				.unwrap_or_else(|_| FedDest::default_port()),
-		))
-	}
-
-	async fn resolve_3_3_use_srv(
-		&self,
-		cache: bool,
-		delegated: &String,
-		overrider: FedDest,
-	) -> Result<FedDest> {
-		debug!("3.3: SRV lookup successful");
-
-		let force_port = overrider.port();
-		self.conditional_query_and_cache_override(
-			delegated,
-			&overrider.hostname(),
-			force_port.unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		if let Some(port) = force_port {
-			return Ok(FedDest::Named(
-				delegated.to_owned(),
-				format!(":{port}")
-					.as_str()
-					.try_into()
-					.unwrap_or_else(|_| FedDest::default_port()),
-			));
-		}
-
-		Ok(add_port_to_hostname(delegated))
-	}
-
-	async fn resolve_3_4_use_default_port(
-		&self,
-		cache: bool,
-		delegated: String,
-	) -> Result<FedDest> {
-		debug!("3.4: No SRV records found, use the hostname from .well-known with default port");
-		self.conditional_query_and_cache(&delegated, DEFAULT_PORT, cache)
-			.await?;
-		Ok(add_port_to_hostname(&delegated))
-	}
-
-	async fn resolve_4_srv_lookup(
-		&self,
-		host: &str,
-		cache: bool,
-		overrider: FedDest,
-	) -> Result<FedDest> {
-		debug!("4: No .well-known; SRV record found");
-		let force_port = overrider.port();
-		self.conditional_query_and_cache_override(
-			host,
-			&overrider.hostname(),
-			force_port.unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		if let Some(port) = force_port {
-			let port = format!(":{port}");
-
-			return Ok(FedDest::Named(
-				host.to_owned(),
-				PortString::from(port.as_str()).unwrap_or_else(|_| FedDest::default_port()),
-			));
-		}
-
-		Ok(add_port_to_hostname(host))
-	}
-
-	async fn resolve_5_direct(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
-		debug!("5: No port provided and no SRV record found");
-		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, cache)
-			.await?;
-
-		Ok(add_port_to_hostname(dest.as_str()))
-	}
-
-	#[inline]
-	async fn conditional_query_and_cache(
-		&self,
-		hostname: &str,
-		port: u16,
-		cache: bool,
-	) -> Result {
-		self.conditional_query_and_cache_override(hostname, hostname, port, cache)
-			.await
-	}
-
-	#[inline]
-	async fn conditional_query_and_cache_override(
-		&self,
-		untername: &str,
-		hostname: &str,
-		port: u16,
-		cache: bool,
-	) -> Result {
-		if !cache {
-			return Ok(());
-		}
-
-		if self.cache.has_override(untername).await {
-			return Ok(());
-		}
-
-		self.query_and_cache_override(untername, hostname, port)
-			.await
-	}
-
-	#[tracing::instrument(name = "ip", level = "debug", skip(self))]
-	async fn query_and_cache_override(
-		&self,
-		untername: &'_ str,
-		hostname: &'_ str,
-		port: u16,
-	) -> Result {
-		self.services.server.check_running()?;
-
-		debug!("querying IP for {untername:?} ({hostname:?}:{port})");
-		match self.resolver.resolver.lookup_ip(hostname.to_owned()).await {
-			| Err(e) => Self::handle_resolve_error(&e, hostname),
-			| Ok(override_ip) => {
-				self.cache.set_override(untername, &CachedOverride {
-					ips: override_ip.into_iter().take(MAX_IPS).collect(),
-					port,
-					expire: CachedOverride::default_expire(),
-					overriding: (hostname != untername)
-						.then_some(hostname.into())
-						.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
-				});
-
-				Ok(())
-			},
-		}
-	}
-
-	#[tracing::instrument(name = "srv", level = "debug", skip(self))]
-	async fn query_srv_record(&self, hostname: &'_ str) -> Result<Option<FedDest>> {
-		let hostnames =
-			[format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")];
-
-		for hostname in hostnames {
-			self.services.server.check_running()?;
-
-			debug!("querying SRV for {hostname:?}");
-			let hostname = hostname.trim_end_matches('.');
-			match self.resolver.resolver.srv_lookup(hostname).await {
-				| Err(e) => Self::handle_resolve_error(&e, hostname)?,
-				| Ok(result) => {
-					return Ok(result.answers().iter().next().map(|result| {
-						let data = result.try_borrow::<SRV>().expect("should be SRV response");
-
-						FedDest::Named(
-							data.data()
-								.target
-								.to_string()
-								.trim_end_matches('.')
-								.to_owned(),
-							format!(":{}", data.data().port)
-								.as_str()
-								.try_into()
-								.unwrap_or_else(|_| FedDest::default_port()),
-						)
-					}));
-				},
-			}
-		}
-
-		Ok(None)
-	}
-
-	fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> {
-		match err {
-			| NetError::NoConnections => {
-				error!(
-					"Your DNS server is overloaded and has ran out of connections. It is \
-					 strongly recommended you remediate this issue to ensure proper federation \
-					 connectivity."
-				);
-
-				Err!(error!(%host, "DNS error: {err}"))
-			},
-			| NetError::Timeout => Err!(error!(%host, "DNS query timed out")),
-			| NetError::Dns(DnsError::NoRecordsFound(..)) => {
-				// Raise to debug_warn if we can find out the result wasn't from cache
-				debug!(%host, "No DNS records found: {err}");
-				Ok(())
-			},
-			| _ => Err!(error!(%host, "DNS error: {err}")),
-		}
-	}
-
-	/// Ensure `dest` is a valid destination (valid ip if it is an IP), and not
-	/// ourselves (unless in config)
-	fn validate_dest(&self, dest: &ServerName) -> Result<()> {
-		if dest == self.services.server.name && !self.services.server.config.federation_loopback {
-			return Err!("Won't send federation request to ourselves");
-		}
-
-		if dest.is_ip_literal() || IPAddress::is_valid(dest.host()) {
-			self.validate_dest_ip_literal(dest)?;
-		}
-
-		debug!(dest = %dest, "Valid destination for resolution");
-		Ok(())
-	}
-
-	fn validate_dest_ip_literal(&self, dest: &ServerName) -> Result<()> {
-		trace!("Destination is an IP literal, checking against IP range denylist.",);
-		debug_assert!(
-			dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
-			"Destination is not an IP literal."
-		);
-		let ip = IPAddress::parse(dest.host()).map_err(|e| {
-			err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}")))
-		})?;
-
-		self.validate_ip(&ip)?;
-
-		Ok(())
-	}
-
-	pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
-		if !self.services.client.valid_cidr_range(ip) {
-			return Err!(BadServerResponse("Not allowed to send requests to this IP"));
-		}
-
-		Ok(())
-	}
-}
diff --git c/src/service/resolver/cache.rs i/src/service/resolver/cache.rs
index 6bad6402d..e775a72ac 100644
--- c/src/service/resolver/cache.rs
+++ i/src/service/resolver/cache.rs
@@ -4,7 +4,7 @@
 	Result,
 	arrayvec::ArrayVec,
 	at, err, implement,
-	utils::{math::Expected, rand, stream::TryIgnore},
+	utils::{math::Expected, stream::TryIgnore},
 };
 use database::{Cbor, Deserialized, Map};
 use futures::{Stream, StreamExt, future::join};
@@ -127,11 +127,6 @@ impl CachedDest {
 	#[must_use]
 	pub fn valid(&self) -> bool { self.expire > SystemTime::now() }

-	#[must_use]
-	pub(crate) fn default_expire() -> SystemTime {
-		rand::time_from_now_secs(60 * 60 * 18..60 * 60 * 36)
-	}
-
 	#[inline]
 	#[must_use]
 	pub fn size(&self) -> usize {
@@ -147,11 +142,6 @@ impl CachedOverride {
 	#[must_use]
 	pub fn valid(&self) -> bool { self.expire > SystemTime::now() }

-	#[must_use]
-	pub(crate) fn default_expire() -> SystemTime {
-		rand::time_from_now_secs(60 * 60 * 6..60 * 60 * 12)
-	}
-
 	#[inline]
 	#[must_use]
 	pub fn size(&self) -> usize { size_of_val(self) }
diff --git c/src/service/resolver/fed.rs i/src/service/resolver/fed.rs
index 83601a98a..f8068f596 100644
--- c/src/service/resolver/fed.rs
+++ i/src/service/resolver/fed.rs
@@ -1,8 +1,4 @@
-use std::{
-	borrow::Cow,
-	fmt,
-	net::{IpAddr, SocketAddr},
-};
+use std::{fmt, net::SocketAddr};

 use conduwuit::{arrayvec::ArrayString, utils::math::Expected};
 use serde::{Deserialize, Serialize};
@@ -18,50 +14,7 @@ pub enum FedDest {

 const DEFAULT_PORT: &str = ":8448";

-/// Attempt to parse `dest_str` as either an IP:port socket pair or as a plain
-/// IP (adding the default port), returning `None` if dest_str is neither a
-/// socket pair nor a plain IP.
-pub(crate) fn get_ip_with_port(dest_str: &str) -> Option<FedDest> {
-	if let Ok(dest) = dest_str.parse::<SocketAddr>() {
-		Some(FedDest::Literal(dest))
-	} else if let Ok(ip_addr) = dest_str.parse::<IpAddr>() {
-		Some(FedDest::Literal(SocketAddr::new(ip_addr, 8448)))
-	} else {
-		None
-	}
-}
-
-/// Convert a `dest` string with or without port into a FedDest with either
-/// the provided port (if host:port format) or the default port (8448)
-pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {
-	let (host, port) = match dest.find(':') {
-		| None => (dest, DEFAULT_PORT),
-		| Some(pos) => dest.split_at(pos),
-	};
-
-	FedDest::Named(
-		host.to_owned(),
-		PortString::from(port).unwrap_or_else(|_| FedDest::default_port()),
-	)
-}
-
-/// Ensure `host` always has a port
-///
-/// `get_ip_with_port` returns `None` if `host` isn't an IP:port string or plain
-/// IP, in which case `add_port_to_hostname` adds it instead
-#[inline]
-pub(crate) fn ensure_host_has_port(host: &str) -> FedDest {
-	get_ip_with_port(host).unwrap_or_else(|| add_port_to_hostname(host))
-}
-
 impl FedDest {
-	pub(crate) fn https_string(&self) -> String {
-		match self {
-			| Self::Literal(addr) => format!("https://{addr}"),
-			| Self::Named(host, port) => format!("https://{host}{port}"),
-		}
-	}
-
 	pub(crate) fn uri_string(&self) -> String {
 		match self {
 			| Self::Literal(addr) => addr.to_string(),
@@ -69,23 +22,6 @@ pub(crate) fn uri_string(&self) -> String {
 		}
 	}

-	#[inline]
-	pub(crate) fn hostname(&self) -> Cow<'_, str> {
-		match &self {
-			| Self::Literal(addr) => addr.ip().to_string().into(),
-			| Self::Named(host, _) => host.into(),
-		}
-	}
-
-	#[inline]
-	#[allow(clippy::string_slice)]
-	pub(crate) fn port(&self) -> Option<u16> {
-		match &self {
-			| Self::Literal(addr) => Some(addr.port()),
-			| Self::Named(_, port) => port[1..].parse().ok(),
-		}
-	}
-
 	#[inline]
 	#[must_use]
 	pub fn default_port() -> PortString {
diff --git c/src/service/resolver/mod.rs i/src/service/resolver/mod.rs
index c513cec9a..c24b8ba60 100644
--- c/src/service/resolver/mod.rs
+++ i/src/service/resolver/mod.rs
@@ -1,33 +1,31 @@
-pub mod actual;
 pub mod cache;
 mod dns;
 pub mod fed;
-#[cfg(test)]
-mod tests;
-mod well_known;

 use std::sync::Arc;

 use async_trait::async_trait;
-use conduwuit::{Result, Server, arrayvec::ArrayString, utils::MutexMap};
+use conduwuit::{Err, Result, implement};
+use ipaddress::IPAddress;
+use resolvematrix::server::MatrixResolver;

 use self::{cache::Cache, dns::Resolver};
 use crate::{Dep, client};

 pub struct Service {
-	pub cache: Arc<Cache>,
-	pub resolver: Arc<Resolver>,
-	resolving: Resolving,
+	pub resolver: MatrixResolver,
+	pub dns: Dns,
 	services: Services,
 }

 struct Services {
-	server: Arc<Server>,
 	client: Dep<client::Service>,
 }

-type Resolving = MutexMap<NameBuf, ()>;
-type NameBuf = ArrayString<256>;
+pub struct Dns {
+	pub cache: Arc<Cache>,
+	pub resolver: Arc<Resolver>,
+}

 #[async_trait]
 impl crate::Service for Service {
@@ -35,20 +33,31 @@ impl crate::Service for Service {
 	fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 		let cache = Cache::new(&args);
 		Ok(Arc::new(Self {
-			cache: cache.clone(),
-			resolver: Resolver::build(args.server, cache)?,
-			resolving: MutexMap::new(),
+			resolver: MatrixResolver::new()?,
+			dns: Dns {
+				cache: cache.clone(),
+				resolver: Resolver::build(args.server, cache)?,
+			},
 			services: Services {
-				server: args.server.clone(),
 				client: args.depend::<client::Service>("client"),
 			},
 		}))
 	}

 	async fn clear_cache(&self) {
-		self.resolver.clear_cache();
-		self.cache.clear().await;
+		// No ability to clean resolvematrix cache at the moment
+		self.dns.resolver.clear_cache();
+		self.dns.cache.clear().await;
 	}

-	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
+	fn name(&self) -> &str { crate::service::make_name(module_path!()) }
+}
+
+#[implement(Service)]
+pub fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
+	if !self.services.client.valid_cidr_range(ip) {
+		return Err!(BadServerResponse("Not allowed to send requests to this IP"));
+	}
+
+	Ok(())
 }
diff --git c/src/service/resolver/tests.rs i/src/service/resolver/tests.rs
deleted file mode 100644
index 068e08bd0..000000000
--- c/src/service/resolver/tests.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use super::fed::{FedDest, add_port_to_hostname, get_ip_with_port};
-
-#[test]
-fn ips_get_default_ports() {
-	assert_eq!(
-		get_ip_with_port("1.1.1.1"),
-		Some(FedDest::Literal("1.1.1.1:8448".parse().unwrap()))
-	);
-	assert_eq!(
-		get_ip_with_port("dead:beef::"),
-		Some(FedDest::Literal("[dead:beef::]:8448".parse().unwrap()))
-	);
-}
-
-#[test]
-fn ips_keep_custom_ports() {
-	assert_eq!(
-		get_ip_with_port("1.1.1.1:1234"),
-		Some(FedDest::Literal("1.1.1.1:1234".parse().unwrap()))
-	);
-	assert_eq!(
-		get_ip_with_port("[dead::beef]:8933"),
-		Some(FedDest::Literal("[dead::beef]:8933".parse().unwrap()))
-	);
-}
-
-#[test]
-fn hostnames_get_default_ports() {
-	assert_eq!(
-		add_port_to_hostname("example.com"),
-		FedDest::Named(String::from("example.com"), ":8448".try_into().unwrap())
-	);
-}
-
-#[test]
-fn hostnames_keep_custom_ports() {
-	assert_eq!(
-		add_port_to_hostname("example.com:1337"),
-		FedDest::Named(String::from("example.com"), ":1337".try_into().unwrap())
-	);
-}
diff --git c/src/service/resolver/well_known.rs i/src/service/resolver/well_known.rs
deleted file mode 100644
index 237c5cd09..000000000
--- c/src/service/resolver/well_known.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use conduwuit::{
-	Result, debug, debug_error, debug_info, implement, trace, utils::response::LimitReadExt,
-};
-use ruma::ServerName;
-
-#[implement(super::Service)]
-#[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))]
-pub(super) async fn request_well_known(&self, dest: &str) -> Result<Option<String>> {
-	trace!("Requesting well known for {dest}");
-	let response = self
-		.services
-		.client
-		.well_known
-		.get(format!("https://{dest}/.well-known/matrix/server"))
-		.send()
-		.await;
-
-	trace!("response: {response:?}");
-	if let Err(e) = &response {
-		debug!("error: {e:?}");
-		return Ok(None);
-	}
-
-	let response = response?;
-	if !response.status().is_success() {
-		debug!("response not 2XX");
-		return Ok(None);
-	}
-
-	let Ok(text) = response.limit_read_text(8192).await else {
-		debug!("failed to read well-known response (too large or non-text content)");
-		return Ok(None);
-	};
-	trace!("response text: {text:?}");
-
-	let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
-
-	let m_server = body
-		.get("m.server")
-		.unwrap_or(&serde_json::Value::Null)
-		.as_str()
-		.unwrap_or_default();
-
-	if ServerName::parse(m_server).is_err() {
-		debug_error!("response content missing or invalid");
-		return Ok(None);
-	}
-
-	debug_info!("{dest:?} found at {m_server:?}");
-	Ok(Some(m_server.to_owned()))
-}

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/admin/debug/commands.rs
#	src/admin/query/resolver.rs
#	src/service/Cargo.toml
#	src/service/client/mod.rs
#	src/service/federation/execute.rs
#	src/service/resolver/cache.rs
#	src/service/resolver/well_known.rs

diff --git c/Cargo.toml i/Cargo.toml
index 09e2f93cb..f89a5e010 100644
--- c/Cargo.toml
+++ i/Cargo.toml
@@ -560,6 +560,9 @@ features = ["std"]
 [workspace.dependencies.nonzero_ext]
 version = "0.3.0"

+[workspace.dependencies.resolvematrix]
+version = "0.1.0"
+
 [workspace.dependencies.serde_urlencoded]
 version = "0.7.1"

diff --git c/src/admin/Cargo.toml i/src/admin/Cargo.toml
index 4afed12b4..1f82978a4 100644
--- c/src/admin/Cargo.toml
+++ i/src/admin/Cargo.toml
@@ -92,6 +92,7 @@ serde-saphyr.workspace = true
 tokio.workspace = true
 tracing-subscriber.workspace = true
 tracing.workspace = true
+resolvematrix.workspace = true

 [lints]
 workspace = true
diff --git c/src/admin/debug/commands.rs i/src/admin/debug/commands.rs
index d1e5c2544..290189859 100644
--- c/src/admin/debug/commands.rs
+++ i/src/admin/debug/commands.rs
@@ -20,6 +20,7 @@
 };
 use futures::{FutureExt, StreamExt, TryStreamExt};
 use lettre::message::Mailbox;
+use resolvematrix::server::{MatrixResolver, ResolvedDestination};
 use ruma::{
 	CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
 	OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, UInt,
@@ -1005,15 +1006,22 @@ pub(super) async fn resolve_true_destination(
 			);
 		}

-		let actual = self
-			.services
-			.resolver
-			.resolve_actual_dest(&server_name, !no_cache)
-			.await?;
+	let resolver: &MatrixResolver = if no_cache {
+		&MatrixResolver::new()?
+	} else {
+		&self.services.resolver.resolver
+	};

-		let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host);
-		self.write_str(&msg).await
-	}
+	let actual = resolver.resolve_server(&server_name.as_str()).await?;
+
+	let destination = match actual.destination {
+		| ResolvedDestination::Literal(addr) => addr.to_string(),
+		| ResolvedDestination::Named(host, port) => format!("{host}:{port}"),
+	};
+
+	let msg = format!("Destination: {}\nHostname URI (SNI): {}", destination, actual.host);
+	self.write_str(&msg).await
+}

 	pub(super) async fn memory_stats(&self, opts: Option<String>) -> Result {
 		const OPTS: &str = "abcdefghijklmnopqrstuvwxyz";
diff --git c/src/admin/query/resolver.rs i/src/admin/query/resolver.rs
index 7e5a4b40d..4a88650f8 100644
--- c/src/admin/query/resolver.rs
+++ i/src/admin/query/resolver.rs
@@ -46,7 +46,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
 		writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
 		writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;

-		let mut destinations = self.services.resolver.cache.destinations().boxed();
+	    let mut destinations = self.services.resolver.dns.cache.destinations().boxed();

 		while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
 			if let Some(server_name) = server_name.as_ref() {
@@ -69,7 +69,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
 		writeln!(self, "| Server Name | IP  | Port | Expires | Overriding |").await?;
 		writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;

-		let mut overrides = self.services.resolver.cache.overrides().boxed();
+		let mut overrides = self.services.resolver.dns.cache.overrides().boxed();

 		while let Some((name, CachedOverride { ips, port, expire, overriding })) =
 			overrides.next().await
@@ -92,11 +92,11 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {

 	async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
 		if all {
-			self.services.resolver.cache.clear().await;
+			self.services.resolver.dns.cache.clear().await;
 			writeln!(self, "Resolver caches cleared!").await
 		} else if let Some(name) = name {
-			self.services.resolver.cache.del_destination(&name);
-			self.services.resolver.cache.del_override(&name);
+			self.services.resolver.dns.cache.del_destination(&name);
+			self.services.resolver.dns.cache.del_override(&name);
 			self.write_str(&format!("Cleared {name} from resolver caches!"))
 				.await
 		} else {
diff --git c/src/core/Cargo.toml i/src/core/Cargo.toml
index 093b50f71..d99184b9d 100644
--- c/src/core/Cargo.toml
+++ i/src/core/Cargo.toml
@@ -117,6 +117,7 @@ url.workspace = true
 parking_lot.workspace = true
 lock_api.workspace = true
 hyper-util.workspace = true
+resolvematrix.workspace = true

 [target.'cfg(unix)'.dependencies]
 nix.workspace = true
diff --git c/src/core/error/mod.rs i/src/core/error/mod.rs
index d61867b01..813324912 100644
--- c/src/core/error/mod.rs
+++ i/src/core/error/mod.rs
@@ -87,6 +87,8 @@ pub enum Error {
 	YamlDe(#[from] serde_saphyr::Error),
 	#[error(transparent)]
 	YamlSer(#[from] serde_saphyr::ser_error::Error),
+	#[error(transparent)]
+	ResolveServer(#[from] resolvematrix::server::ResolveServerError),

 	// ruma/conduwuit
 	#[error("Arithmetic operation failed: {0}")]
diff --git c/src/service/Cargo.toml i/src/service/Cargo.toml
index e302ff6c9..923fef2cb 100644
--- c/src/service/Cargo.toml
+++ i/src/service/Cargo.toml
@@ -119,6 +119,7 @@ recaptcha-verify = { version = "0.2.0", default-features = false }
 reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"]  } # As long as recaptcha-verify's reqwest is outdated
 yansi.workspace = true
 lettre.workspace = true
+resolvematrix.workspace = true
 serde_urlencoded.workspace = true

 [target.'cfg(all(unix, target_os = "linux"))'.dependencies]
diff --git c/src/service/client/mod.rs i/src/service/client/mod.rs
index b4c645ba2..4a299fee3 100644
--- c/src/service/client/mod.rs
+++ i/src/service/client/mod.rs
@@ -43,7 +43,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {

 		Ok(Arc::new(Self {
 			default: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.build()?,

 			url_preview: base(config)
@@ -51,19 +51,19 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 					builder_interface(builder, url_preview_bind_iface.as_deref())
 				})?
 				.local_address(url_preview_bind_addr)
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.timeout(Duration::from_secs(config.url_preview_timeout))
 				.redirect(redirect::Policy::limited(3))
 				.user_agent(url_preview_user_agent)
 				.build()?,

 			extern_media: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.redirect(redirect::Policy::limited(3))
 				.build()?,

 			well_known: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.connect_timeout(Duration::from_secs(config.well_known_conn_timeout))
 				.read_timeout(Duration::from_secs(config.well_known_timeout))
 				.timeout(Duration::from_secs(config.well_known_timeout))
@@ -72,7 +72,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			federation: base(config)?
-				.dns_resolver(resolver.resolver.hooked.clone())
+				.dns_resolver(resolver.dns.resolver.hooked.clone())
 				.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
 				.read_timeout(Duration::from_secs(config.federation_timeout))
 				.timeout(Duration::from_secs(
@@ -86,7 +86,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			federation_slow: base(config)?
-				.dns_resolver(resolver.resolver.hooked.clone())
+				.dns_resolver(resolver.dns.resolver.hooked.clone())
 				.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
 				.read_timeout(Duration::from_secs(config.federation_timeout.saturating_mul(6)))
 				.timeout(Duration::from_secs(
@@ -100,7 +100,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			sender: base(config)?
-				.dns_resolver(resolver.resolver.hooked.clone())
+				.dns_resolver(resolver.dns.resolver.hooked.clone())
 				.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
 				.read_timeout(Duration::from_secs(config.sender_timeout))
 				.timeout(Duration::from_secs(config.sender_timeout))
@@ -110,7 +110,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			appservice: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.connect_timeout(Duration::from_secs(5))
 				.read_timeout(Duration::from_secs(config.appservice_timeout))
 				.timeout(Duration::from_secs(config.appservice_timeout))
@@ -120,7 +120,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 				.build()?,

 			pusher: base(config)?
-				.dns_resolver(resolver.resolver.clone())
+				.dns_resolver(resolver.dns.resolver.clone())
 				.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
 				.timeout(Duration::from_secs(config.pusher_timeout))
 				.pool_max_idle_per_host(1)
diff --git c/src/service/federation/execute.rs i/src/service/federation/execute.rs
index 70bef2f0e..d843f9ad4 100644
--- c/src/service/federation/execute.rs
+++ i/src/service/federation/execute.rs
@@ -7,6 +7,7 @@
 };
 use ipaddress::IPAddress;
 use reqwest::{Client, Method, Request, Response, Url};
+use resolvematrix::resolution::Resolution;
 use ruma::{
 	ServerName,
 	api::{
@@ -150,7 +151,7 @@ pub async fn execute_on<'i, T, PathBuilderInput>(
 	async fn perform<T>(
 		&self,
 		dest: &ServerName,
-		actual: &ActualDest,
+		actual: &Resolution,
 		request: Request,
 		client: &Client,
 	) -> Result<T::IncomingResponse>
@@ -198,7 +199,7 @@ fn validate_url(&self, url: &Url) -> Result<()> {
 	async fn handle_response<T>(
 		&self,
 		dest: &ServerName,
-		actual: &ActualDest,
+		actual: &Resolution,
 		method: &Method,
 		url: &Url,
 		response: Response,
@@ -231,7 +232,7 @@ async fn handle_response<T>(

 async fn into_http_response(
 	dest: &ServerName,
-	actual: &ActualDest,
+	actual: &Resolution,
 	method: &Method,
 	url: &Url,
 	mut response: Response,
@@ -243,7 +244,7 @@ async fn into_http_response(
 		request_url = %url,
 		response_url = %response.url(),
 		"Received response from {}",
-		actual.string(),
+		actual.base_url(),
 	);

 	let mut http_response_builder = http::Response::builder()
@@ -280,7 +281,7 @@ async fn into_http_response(
 }

 fn handle_error(
-	actual: &ActualDest,
+	actual: &Resolution,
 	method: &Method,
 	url: &Url,
 	mut e: reqwest::Error,
diff --git c/src/service/resolver/actual.rs i/src/service/resolver/actual.rs
index 1ca3b595c..e69de29bb 100644
--- c/src/service/resolver/actual.rs
+++ i/src/service/resolver/actual.rs
@@ -1,441 +0,0 @@
-use std::fmt::Debug;
-
-use conduwuit::{Err, Result, debug, debug_info, err, error, trace};
-use futures::{FutureExt, TryFutureExt};
-use hickory_resolver::{
-	net::{DnsError, NetError},
-	proto::rr::rdata::SRV,
-};
-use ipaddress::IPAddress;
-use ruma::ServerName;
-
-use super::{
-	cache::{CachedDest, CachedOverride, MAX_IPS},
-	fed::{FedDest, PortString, add_port_to_hostname, ensure_host_has_port, get_ip_with_port},
-};
-
-const DEFAULT_PORT: u16 = 8448;
-
-#[derive(Clone, Debug)]
-pub(crate) struct ActualDest {
-	pub(crate) dest: FedDest,
-	pub(crate) host: String,
-}
-
-impl ActualDest {
-	#[inline]
-	pub(crate) fn string(&self) -> String { self.dest.https_string() }
-}
-
-impl super::Service {
-	#[tracing::instrument(skip_all, level = "debug", name = "resolve")]
-	pub(crate) async fn get_actual_dest(&self, server_name: &ServerName) -> Result<ActualDest> {
-		let (CachedDest { dest, host, .. }, _cached) =
-			self.lookup_actual_dest(server_name).await?;
-
-		Ok(ActualDest { dest, host })
-	}
-
-	pub(crate) async fn lookup_actual_dest(
-		&self,
-		server_name: &ServerName,
-	) -> Result<(CachedDest, bool)> {
-		if let Ok(result) = self.cache.get_destination(server_name).await {
-			return Ok((result, true));
-		}
-
-		let _dedup = self.resolving.lock(server_name.as_str());
-		if let Ok(result) = self.cache.get_destination(server_name).await {
-			return Ok((result, true));
-		}
-
-		self.resolve_actual_dest(server_name, true)
-			.inspect_ok(|result| self.cache.set_destination(server_name, result))
-			.map_ok(|result| (result, false))
-			.boxed()
-			.await
-	}
-
-	/// Returns: `actual_destination` + `Host` http header
-	#[tracing::instrument(name = "actual", level = "debug", skip(self, cache))]
-	pub async fn resolve_actual_dest(
-		&self,
-		dest: &ServerName,
-		cache: bool,
-	) -> Result<CachedDest> {
-		debug!(
-			dest = %dest,
-			cache = %cache,
-			"Resolving server name and port"
-		);
-		// Ensure dest is a valid connection endpoint
-		self.validate_dest(dest)?;
-
-		// Clippy believes this can be a clone, however we are actually converting
-		// ServerName to String
-		#[allow(clippy::implicit_clone)]
-		let mut host_header = dest.to_string().to_owned();
-		let actual_dest = self
-			.resolve_server_name(dest, cache, &mut host_header)
-			.await?;
-
-		host_header = ensure_host_has_port(&host_header).to_string();
-
-		debug!(
-			dest = %dest,
-			actual_dest = %actual_dest,
-			host = %host_header,
-			"Finished resolving server name"
-		);
-		Ok(CachedDest {
-			dest: actual_dest,
-			host: host_header,
-			expire: CachedDest::default_expire(),
-		})
-	}
-
-	/// Performs the server resolution steps as per the specification:
-	/// <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
-	async fn resolve_server_name(
-		&self,
-		dest: &ServerName,
-		cache: bool,
-		host: &mut String,
-	) -> Result<FedDest> {
-		// 1. If `dest` is an IP, use it directly. If a port is provided as well
-		//    (IP:port socket pair) use that, otherwise default to port 8448
-		if let Some(fed_dest) = get_ip_with_port(dest.as_str()) {
-			debug!("1: IP literal with provided or default port");
-			return Ok(fed_dest);
-		}
-
-		// 2. If `dest` is a hostname and has a provided port (format of `host:port`),
-		//    resolve the hostname to an IP address and connect it and the provided port
-		if dest.as_str().contains(':') {
-			return self.resolve_2_host_port(dest, cache).await;
-		}
-
-		// Pre-resolve IP? Unsure what overrides exactly do, system is due to be removed
-		// either way https://matrix.to/#/!da26JtAjE6APGLnX8ncWsvc-skF2KQZ9Nw_MbNpYD2k/%24_hq6JP0JXANbMTMPdV64iZbgbsZdhy92M5ndDYGy6No
-		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, true)
-			.await?;
-
-		// Ensure server is running (not shutting down) before continuing resolution
-		self.services.server.check_running()?;
-
-		// 3. If `dest` is a hostname with no port, send GET to `https://<dest>/.well-known/matrix/server`.
-		//    If invalid JSON (throws error), skip to step 4. Otherwise, parse
-		//    `delegated` as `<hostname>[:<port>]` and...
-		if let Some(delegated) = self.request_well_known(dest.as_str()).await? {
-			return self.resolve_3_well_known(host, cache, delegated).await;
-		}
-
-		// 4. if .well-known errored, perform SRV (see 3.3)
-		if let Some(overrider) = self.query_srv_record(dest.as_str()).await? {
-			return self.resolve_4_srv_lookup(host, cache, overrider).await;
-		}
-
-		// 5. if .well-known errored and no SRV exists, resolve IP and connect on
-		//    default port (8448)
-		self.resolve_5_direct(dest, cache).await
-	}
-
-	/// Parse a host:port socket pair into separate parts, and resolve the
-	/// hostname into an IP address
-	async fn resolve_2_host_port(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
-		debug!("2: Hostname with included port");
-		let (host, port) = dest.as_str().split_once(':').unwrap();
-
-		self.conditional_query_and_cache(
-			host,
-			port.parse::<u16>().unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		Ok(FedDest::Named(
-			host.to_owned(),
-			port.try_into().unwrap_or_else(|_| FedDest::default_port()),
-		))
-	}
-
-	async fn resolve_3_well_known(
-		&self,
-		host: &mut String,
-		cache: bool,
-		delegated: String,
-	) -> Result<FedDest> {
-		debug!("3: A .well-known file is available");
-		*host = add_port_to_hostname(&delegated).uri_string();
-
-		// 3.1 - If <delegated> is IP:port, connect to that,
-		//       or IP with default port if no port provided (8448)
-		if let Some(host_and_port) = get_ip_with_port(&delegated) {
-			debug!("3.1: IP with port in .well-known file");
-			return Ok(host_and_port);
-		}
-
-		// 3.2 - If <delegated> is hostname:port, lookup IP for hostname and connect
-		if delegated.contains(':') {
-			return self.resolve_3_2_hostname_port(cache, &delegated).await;
-		}
-
-		// 3.3 - If <delegated> is not an IP and there is no port, lookup SRV
-		// `_matrix._tcp.<delegated>` (which may provide a new hostname + port to use,
-		// see steps 3.1 and 3.2)
-		trace!("Delegated hostname has no port, querying SRV");
-		if let Some(overrider) = self.query_srv_record(&delegated).await? {
-			return self.resolve_3_3_use_srv(cache, &delegated, overrider).await;
-		}
-
-		self.resolve_3_4_use_default_port(cache, delegated).await
-	}
-
-	async fn resolve_3_2_hostname_port(&self, cache: bool, delegated: &str) -> Result<FedDest> {
-		debug!("3.2: Hostname with port in .well-known file");
-		let (host, port) = &delegated.split_once(':').unwrap();
-		self.conditional_query_and_cache(
-			host,
-			port.parse::<u16>().unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		trace!("Successfully resolved IP for {delegated}");
-		Ok(FedDest::Named(
-			host.to_owned().to_owned(),
-			port.to_owned()
-				.try_into()
-				.unwrap_or_else(|_| FedDest::default_port()),
-		))
-	}
-
-	async fn resolve_3_3_use_srv(
-		&self,
-		cache: bool,
-		delegated: &String,
-		overrider: FedDest,
-	) -> Result<FedDest> {
-		debug!("3.3: SRV lookup successful");
-
-		let force_port = overrider.port();
-		self.conditional_query_and_cache_override(
-			delegated,
-			&overrider.hostname(),
-			force_port.unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		if let Some(port) = force_port {
-			return Ok(FedDest::Named(
-				delegated.to_owned(),
-				format!(":{port}")
-					.as_str()
-					.try_into()
-					.unwrap_or_else(|_| FedDest::default_port()),
-			));
-		}
-
-		Ok(add_port_to_hostname(delegated))
-	}
-
-	async fn resolve_3_4_use_default_port(
-		&self,
-		cache: bool,
-		delegated: String,
-	) -> Result<FedDest> {
-		debug!("3.4: No SRV records found, use the hostname from .well-known with default port");
-		self.conditional_query_and_cache(&delegated, DEFAULT_PORT, cache)
-			.await?;
-		Ok(add_port_to_hostname(&delegated))
-	}
-
-	async fn resolve_4_srv_lookup(
-		&self,
-		host: &str,
-		cache: bool,
-		overrider: FedDest,
-	) -> Result<FedDest> {
-		debug!("4: No .well-known; SRV record found");
-		let force_port = overrider.port();
-		self.conditional_query_and_cache_override(
-			host,
-			&overrider.hostname(),
-			force_port.unwrap_or(DEFAULT_PORT),
-			cache,
-		)
-		.await?;
-
-		if let Some(port) = force_port {
-			let port = format!(":{port}");
-
-			return Ok(FedDest::Named(
-				host.to_owned(),
-				PortString::from(port.as_str()).unwrap_or_else(|_| FedDest::default_port()),
-			));
-		}
-
-		Ok(add_port_to_hostname(host))
-	}
-
-	async fn resolve_5_direct(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
-		debug!("5: No port provided and no SRV record found");
-		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, cache)
-			.await?;
-
-		Ok(add_port_to_hostname(dest.as_str()))
-	}
-
-	#[inline]
-	async fn conditional_query_and_cache(
-		&self,
-		hostname: &str,
-		port: u16,
-		cache: bool,
-	) -> Result {
-		self.conditional_query_and_cache_override(hostname, hostname, port, cache)
-			.await
-	}
-
-	#[inline]
-	async fn conditional_query_and_cache_override(
-		&self,
-		untername: &str,
-		hostname: &str,
-		port: u16,
-		cache: bool,
-	) -> Result {
-		if !cache {
-			return Ok(());
-		}
-
-		if self.cache.has_override(untername).await {
-			return Ok(());
-		}
-
-		self.query_and_cache_override(untername, hostname, port)
-			.await
-	}
-
-	#[tracing::instrument(name = "ip", level = "debug", skip(self))]
-	async fn query_and_cache_override(
-		&self,
-		untername: &'_ str,
-		hostname: &'_ str,
-		port: u16,
-	) -> Result {
-		self.services.server.check_running()?;
-
-		debug!("querying IP for {untername:?} ({hostname:?}:{port})");
-		match self.resolver.resolver.lookup_ip(hostname.to_owned()).await {
-			| Err(e) => Self::handle_resolve_error(&e, hostname),
-			| Ok(override_ip) => {
-				self.cache.set_override(untername, &CachedOverride {
-					ips: override_ip.into_iter().take(MAX_IPS).collect(),
-					port,
-					expire: CachedOverride::default_expire(),
-					overriding: (hostname != untername)
-						.then_some(hostname.into())
-						.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
-				});
-
-				Ok(())
-			},
-		}
-	}
-
-	#[tracing::instrument(name = "srv", level = "debug", skip(self))]
-	async fn query_srv_record(&self, hostname: &'_ str) -> Result<Option<FedDest>> {
-		let hostnames =
-			[format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")];
-
-		for hostname in hostnames {
-			self.services.server.check_running()?;
-
-			debug!("querying SRV for {hostname:?}");
-			let hostname = hostname.trim_end_matches('.');
-			match self.resolver.resolver.srv_lookup(hostname).await {
-				| Err(e) => Self::handle_resolve_error(&e, hostname)?,
-				| Ok(result) => {
-					return Ok(result.answers().iter().next().map(|result| {
-						let data = result.try_borrow::<SRV>().expect("should be SRV response");
-
-						FedDest::Named(
-							data.data()
-								.target
-								.to_string()
-								.trim_end_matches('.')
-								.to_owned(),
-							format!(":{}", data.data().port)
-								.as_str()
-								.try_into()
-								.unwrap_or_else(|_| FedDest::default_port()),
-						)
-					}));
-				},
-			}
-		}
-
-		Ok(None)
-	}
-
-	fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> {
-		match err {
-			| NetError::NoConnections => {
-				error!(
-					"Your DNS server is overloaded and has ran out of connections. It is \
-					 strongly recommended you remediate this issue to ensure proper federation \
-					 connectivity."
-				);
-
-				Err!(error!(%host, "DNS error: {err}"))
-			},
-			| NetError::Timeout => Err!(error!(%host, "DNS query timed out")),
-			| NetError::Dns(DnsError::NoRecordsFound(..)) => {
-				// Raise to debug_warn if we can find out the result wasn't from cache
-				debug!(%host, "No DNS records found: {err}");
-				Ok(())
-			},
-			| _ => Err!(error!(%host, "DNS error: {err}")),
-		}
-	}
-
-	/// Ensure `dest` is a valid destination (valid ip if it is an IP), and not
-	/// ourselves (unless in config)
-	fn validate_dest(&self, dest: &ServerName) -> Result<()> {
-		if dest == self.services.server.name && !self.services.server.config.federation_loopback {
-			return Err!("Won't send federation request to ourselves");
-		}
-
-		if dest.is_ip_literal() || IPAddress::is_valid(dest.host()) {
-			self.validate_dest_ip_literal(dest)?;
-		}
-
-		debug!(dest = %dest, "Valid destination for resolution");
-		Ok(())
-	}
-
-	fn validate_dest_ip_literal(&self, dest: &ServerName) -> Result<()> {
-		trace!("Destination is an IP literal, checking against IP range denylist.",);
-		debug_assert!(
-			dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
-			"Destination is not an IP literal."
-		);
-		let ip = IPAddress::parse(dest.host()).map_err(|e| {
-			err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}")))
-		})?;
-
-		self.validate_ip(&ip)?;
-
-		Ok(())
-	}
-
-	pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
-		if !self.services.client.valid_cidr_range(ip) {
-			return Err!(BadServerResponse("Not allowed to send requests to this IP"));
-		}
-
-		Ok(())
-	}
-}
diff --git c/src/service/resolver/cache.rs i/src/service/resolver/cache.rs
index b0268a3cd..6ec0b5b4d 100644
--- c/src/service/resolver/cache.rs
+++ i/src/service/resolver/cache.rs
@@ -4,7 +4,7 @@
 	Result,
 	arrayvec::ArrayVec,
 	at, err,
-	utils::{math::Expected, rand, stream::TryIgnore},
+	utils::{math::Expected, stream::TryIgnore},
 };
 use database::{Cbor, Deserialized, Map};
 use futures::{Stream, StreamExt, future::join};
@@ -114,11 +114,6 @@ impl CachedDest {
 	#[must_use]
 	pub fn valid(&self) -> bool { self.expire > SystemTime::now() }

-	#[must_use]
-	pub(crate) fn default_expire() -> SystemTime {
-		rand::time_from_now_secs(60 * 60 * 18..60 * 60 * 36)
-	}
-
 	#[inline]
 	#[must_use]
 	pub fn size(&self) -> usize {
@@ -134,11 +129,6 @@ impl CachedOverride {
 	#[must_use]
 	pub fn valid(&self) -> bool { self.expire > SystemTime::now() }

-	#[must_use]
-	pub(crate) fn default_expire() -> SystemTime {
-		rand::time_from_now_secs(60 * 60 * 6..60 * 60 * 12)
-	}
-
 	#[inline]
 	#[must_use]
 	pub fn size(&self) -> usize { size_of_val(self) }
diff --git c/src/service/resolver/fed.rs i/src/service/resolver/fed.rs
index 83601a98a..f8068f596 100644
--- c/src/service/resolver/fed.rs
+++ i/src/service/resolver/fed.rs
@@ -1,8 +1,4 @@
-use std::{
-	borrow::Cow,
-	fmt,
-	net::{IpAddr, SocketAddr},
-};
+use std::{fmt, net::SocketAddr};

 use conduwuit::{arrayvec::ArrayString, utils::math::Expected};
 use serde::{Deserialize, Serialize};
@@ -18,50 +14,7 @@ pub enum FedDest {

 const DEFAULT_PORT: &str = ":8448";

-/// Attempt to parse `dest_str` as either an IP:port socket pair or as a plain
-/// IP (adding the default port), returning `None` if dest_str is neither a
-/// socket pair nor a plain IP.
-pub(crate) fn get_ip_with_port(dest_str: &str) -> Option<FedDest> {
-	if let Ok(dest) = dest_str.parse::<SocketAddr>() {
-		Some(FedDest::Literal(dest))
-	} else if let Ok(ip_addr) = dest_str.parse::<IpAddr>() {
-		Some(FedDest::Literal(SocketAddr::new(ip_addr, 8448)))
-	} else {
-		None
-	}
-}
-
-/// Convert a `dest` string with or without port into a FedDest with either
-/// the provided port (if host:port format) or the default port (8448)
-pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {
-	let (host, port) = match dest.find(':') {
-		| None => (dest, DEFAULT_PORT),
-		| Some(pos) => dest.split_at(pos),
-	};
-
-	FedDest::Named(
-		host.to_owned(),
-		PortString::from(port).unwrap_or_else(|_| FedDest::default_port()),
-	)
-}
-
-/// Ensure `host` always has a port
-///
-/// `get_ip_with_port` returns `None` if `host` isn't an IP:port string or plain
-/// IP, in which case `add_port_to_hostname` adds it instead
-#[inline]
-pub(crate) fn ensure_host_has_port(host: &str) -> FedDest {
-	get_ip_with_port(host).unwrap_or_else(|| add_port_to_hostname(host))
-}
-
 impl FedDest {
-	pub(crate) fn https_string(&self) -> String {
-		match self {
-			| Self::Literal(addr) => format!("https://{addr}"),
-			| Self::Named(host, port) => format!("https://{host}{port}"),
-		}
-	}
-
 	pub(crate) fn uri_string(&self) -> String {
 		match self {
 			| Self::Literal(addr) => addr.to_string(),
@@ -69,23 +22,6 @@ pub(crate) fn uri_string(&self) -> String {
 		}
 	}

-	#[inline]
-	pub(crate) fn hostname(&self) -> Cow<'_, str> {
-		match &self {
-			| Self::Literal(addr) => addr.ip().to_string().into(),
-			| Self::Named(host, _) => host.into(),
-		}
-	}
-
-	#[inline]
-	#[allow(clippy::string_slice)]
-	pub(crate) fn port(&self) -> Option<u16> {
-		match &self {
-			| Self::Literal(addr) => Some(addr.port()),
-			| Self::Named(_, port) => port[1..].parse().ok(),
-		}
-	}
-
 	#[inline]
 	#[must_use]
 	pub fn default_port() -> PortString {
diff --git c/src/service/resolver/mod.rs i/src/service/resolver/mod.rs
index c513cec9a..c24b8ba60 100644
--- c/src/service/resolver/mod.rs
+++ i/src/service/resolver/mod.rs
@@ -1,33 +1,31 @@
-pub mod actual;
 pub mod cache;
 mod dns;
 pub mod fed;
-#[cfg(test)]
-mod tests;
-mod well_known;

 use std::sync::Arc;

 use async_trait::async_trait;
-use conduwuit::{Result, Server, arrayvec::ArrayString, utils::MutexMap};
+use conduwuit::{Err, Result, implement};
+use ipaddress::IPAddress;
+use resolvematrix::server::MatrixResolver;

 use self::{cache::Cache, dns::Resolver};
 use crate::{Dep, client};

 pub struct Service {
-	pub cache: Arc<Cache>,
-	pub resolver: Arc<Resolver>,
-	resolving: Resolving,
+	pub resolver: MatrixResolver,
+	pub dns: Dns,
 	services: Services,
 }

 struct Services {
-	server: Arc<Server>,
 	client: Dep<client::Service>,
 }

-type Resolving = MutexMap<NameBuf, ()>;
-type NameBuf = ArrayString<256>;
+pub struct Dns {
+	pub cache: Arc<Cache>,
+	pub resolver: Arc<Resolver>,
+}

 #[async_trait]
 impl crate::Service for Service {
@@ -35,20 +33,31 @@ impl crate::Service for Service {
 	fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
 		let cache = Cache::new(&args);
 		Ok(Arc::new(Self {
-			cache: cache.clone(),
-			resolver: Resolver::build(args.server, cache)?,
-			resolving: MutexMap::new(),
+			resolver: MatrixResolver::new()?,
+			dns: Dns {
+				cache: cache.clone(),
+				resolver: Resolver::build(args.server, cache)?,
+			},
 			services: Services {
-				server: args.server.clone(),
 				client: args.depend::<client::Service>("client"),
 			},
 		}))
 	}

 	async fn clear_cache(&self) {
-		self.resolver.clear_cache();
-		self.cache.clear().await;
+		// No ability to clean resolvematrix cache at the moment
+		self.dns.resolver.clear_cache();
+		self.dns.cache.clear().await;
 	}

-	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
+	fn name(&self) -> &str { crate::service::make_name(module_path!()) }
+}
+
+#[implement(Service)]
+pub fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
+	if !self.services.client.valid_cidr_range(ip) {
+		return Err!(BadServerResponse("Not allowed to send requests to this IP"));
+	}
+
+	Ok(())
 }
diff --git c/src/service/resolver/tests.rs i/src/service/resolver/tests.rs
deleted file mode 100644
index 068e08bd0..000000000
--- c/src/service/resolver/tests.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use super::fed::{FedDest, add_port_to_hostname, get_ip_with_port};
-
-#[test]
-fn ips_get_default_ports() {
-	assert_eq!(
-		get_ip_with_port("1.1.1.1"),
-		Some(FedDest::Literal("1.1.1.1:8448".parse().unwrap()))
-	);
-	assert_eq!(
-		get_ip_with_port("dead:beef::"),
-		Some(FedDest::Literal("[dead:beef::]:8448".parse().unwrap()))
-	);
-}
-
-#[test]
-fn ips_keep_custom_ports() {
-	assert_eq!(
-		get_ip_with_port("1.1.1.1:1234"),
-		Some(FedDest::Literal("1.1.1.1:1234".parse().unwrap()))
-	);
-	assert_eq!(
-		get_ip_with_port("[dead::beef]:8933"),
-		Some(FedDest::Literal("[dead::beef]:8933".parse().unwrap()))
-	);
-}
-
-#[test]
-fn hostnames_get_default_ports() {
-	assert_eq!(
-		add_port_to_hostname("example.com"),
-		FedDest::Named(String::from("example.com"), ":8448".try_into().unwrap())
-	);
-}
-
-#[test]
-fn hostnames_keep_custom_ports() {
-	assert_eq!(
-		add_port_to_hostname("example.com:1337"),
-		FedDest::Named(String::from("example.com"), ":1337".try_into().unwrap())
-	);
-}
diff --git c/src/service/resolver/well_known.rs i/src/service/resolver/well_known.rs
deleted file mode 100644
index 2a4ecf222..000000000
--- c/src/service/resolver/well_known.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use conduwuit::{Result, debug, debug_error, debug_info, trace, utils::response::LimitReadExt};
-use ruma::ServerName;
-
-impl super::Service {
-	#[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))]
-	pub(super) async fn request_well_known(&self, dest: &str) -> Result<Option<String>> {
-		trace!("Requesting well known for {dest}");
-		let response = self
-			.services
-			.client
-			.well_known
-			.get(format!("https://{dest}/.well-known/matrix/server"))
-			.send()
-			.await;
-
-		trace!("response: {response:?}");
-		if let Err(e) = &response {
-			debug!("error: {e:?}");
-			return Ok(None);
-		}
-
-		let response = response?;
-		if !response.status().is_success() {
-			debug!("response not 2XX");
-			return Ok(None);
-		}
-
-		let Ok(text) = response.limit_read_text(8192).await else {
-			debug!("failed to read well-known response (too large or non-text content)");
-			return Ok(None);
-		};
-		trace!("response text: {text:?}");
-
-		let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
-
-		let m_server = body
-			.get("m.server")
-			.unwrap_or(&serde_json::Value::Null)
-			.as_str()
-			.unwrap_or_default();
-
-		if ServerName::parse(m_server).is_err() {
-			debug_error!("response content missing or invalid");
-			return Ok(None);
-		}
-
-		debug_info!("{dest:?} found at {m_server:?}");
-		Ok(Some(m_server.to_owned()))
-	}
-}
2026-07-03 10:59:11 +02:00
theS1LV3R 3a90e4eaac chore: Rename variables, fix comments, misc cleanup 2026-07-03 10:59:10 +02:00
theS1LV3R a3ffd4d9c4 fix: Improve splitting of hostname:port, fixed returns 2026-07-03 10:59:10 +02:00
theS1LV3R da03baa063 fix: Ensure codeflow returns at correct locations 2026-07-03 10:59:10 +02:00
theS1LV3R f075e1ce5b chore: Remove useless comments 2026-07-03 10:59:10 +02:00
theS1LV3R 03e312f2b4 chore: Formatting + Commenting
diff --git c/src/service/resolver/actual.rs i/src/service/resolver/actual.rs
index 9cd1aec15..495d83172 100644
--- c/src/service/resolver/actual.rs
+++ i/src/service/resolver/actual.rs
@@ -1,9 +1,5 @@
 use std::fmt::Debug;

-use super::{
-	cache::{CachedDest, CachedOverride, MAX_IPS},
-	fed::{FedDest, PortString, add_port_to_hostname, ensure_host_has_port, get_ip_with_port},
-};
 use conduwuit::{Err, Result, debug, debug_info, err, error, trace};
 use futures::{FutureExt, TryFutureExt};
 use hickory_resolver::{
@@ -15,9 +11,11 @@

 use super::{
 	cache::{CachedDest, CachedOverride, MAX_IPS},
-	fed::{FedDest, PortString, add_port_to_hostname, get_ip_with_port},
+	fed::{FedDest, PortString, add_port_to_hostname, ensure_host_has_port, get_ip_with_port},
 };

+const DEFAULT_PORT: u16 = 8448;
+
 #[derive(Clone, Debug)]
 pub(crate) struct ActualDest {
 	pub(crate) dest: FedDest,
@@ -26,9 +24,7 @@ pub(crate) struct ActualDest {

 impl ActualDest {
 	#[inline]
-	pub(crate) fn string(&self) -> String {
-		self.dest.https_string()
-	}
+	pub(crate) fn string(&self) -> String { self.dest.https_string() }
 }

 impl super::Service {
@@ -75,7 +71,8 @@ pub async fn resolve_actual_dest(
 		// Ensure dest is a valid connection endpoint
 		self.validate_dest(dest)?;

-		// Clippy believes this can be a clone, however we are actually converting ServerName to String
+		// Clippy believes this can be a clone, however we are actually converting
+		// ServerName to String
 		#[allow(clippy::implicit_clone)]
 		let mut host = dest.to_string().to_owned();
 		let actual_dest = self.resolve_server_name(dest, cache, &mut host).await?;
@@ -103,8 +100,8 @@ async fn resolve_server_name(
 		cache: bool,
 		host: &mut String,
 	) -> Result<FedDest> {
-		// 1. If `dest` is an IP, use it directly. If a port is provided as well (IP:port socket pair)
-		//    use that, otherwise default to port 8448
+		// 1. If `dest` is an IP, use it directly. If a port is provided as well
+		//    (IP:port socket pair) use that, otherwise default to port 8448
 		if let Some(fed_dest) = get_ip_with_port(dest.as_str()) {
 			debug!("1: IP literal with provided or default port");
 			return Ok(fed_dest);
@@ -117,8 +114,8 @@ async fn resolve_server_name(
 				.await?;
 		}

-		// Pre-resolve IP? Unsure what overrides exactly do, system is due to be removed either way
-		// https://matrix.to/#/!da26JtAjE6APGLnX8ncWsvc-skF2KQZ9Nw_MbNpYD2k/%24_hq6JP0JXANbMTMPdV64iZbgbsZdhy92M5ndDYGy6No
+		// Pre-resolve IP? Unsure what overrides exactly do, system is due to be removed
+		// either way https://matrix.to/#/!da26JtAjE6APGLnX8ncWsvc-skF2KQZ9Nw_MbNpYD2k/%24_hq6JP0JXANbMTMPdV64iZbgbsZdhy92M5ndDYGy6No
 		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, true)
 			.await?;

@@ -126,7 +123,8 @@ async fn resolve_server_name(
 		self.services.server.check_running()?;

 		// 3. If `dest` is a hostname with no port, send GET to `https://<dest>/.well-known/matrix/server`.
-		// If invalid JSON (throws error), skip to step 4. Otherwise, parse `delegated` as `<hostname>[:<port>]` and...
+		// If invalid JSON (throws error), skip to step 4. Otherwise, parse `delegated`
+		// as `<hostname>[:<port>]` and...
 		if let Some(delegated) = self.request_well_known(dest.as_str()).await? {
 			// delegated=matrix-federation.matrix.org:443 // host=matrix.org
 			self.resolve_3_well_known(host, cache, delegated).await?;
@@ -137,11 +135,13 @@ async fn resolve_server_name(
 			self.resolve_4_srv_lookup(host, cache, overrider).await?;
 		}

-		// 5. if .well-known errored and no SRV exists, resolve IP and connect on default port (8448)
+		// 5. if .well-known errored and no SRV exists, resolve IP and connect on
+		//    default port (8448)
 		self.resolve_5_direct(dest, cache).await
 	}

-	/// Parse a host:port socket pair into separate parts, and resolve the hostname into an IP address
+	/// Parse a host:port socket pair into separate parts, and resolve the
+	/// hostname into an IP address
 	async fn resolve_2_host_port(
 		&self,
 		dest: &ServerName,
@@ -180,14 +180,16 @@ async fn resolve_3_well_known(
 			return Ok(host_and_port);
 		}

-		// 3.2 - If <delegated> is not an IP and a port is present, lookup IP for hostname and connect
+		// 3.2 - If <delegated> is not an IP and a port is present, lookup IP for
+		// hostname and connect
 		if let Some(pos) = &delegated.find(':') {
 			self.resolve_3_2_hostname_port(cache, &delegated, *pos)
 				.await?;
 		}

-		// 3.3 - If <delegated> is not an IP and there is no port, lookup SRV `_matrix._tcp.<delegated>`
-		// (which may provide a new hostname + port to use, see steps 3.1 and 3.2)
+		// 3.3 - If <delegated> is not an IP and there is no port, lookup SRV
+		// `_matrix._tcp.<delegated>` (which may provide a new hostname + port to use,
+		// see steps 3.1 and 3.2)
 		trace!("Delegated hostname has no port, querying SRV");
 		if let Some(overrider) = self.query_srv_record(&delegated).await? {
 			self.resolve_3_3_use_srv(cache, &delegated, overrider)
@@ -342,17 +344,14 @@ async fn query_and_cache_override(
 		match self.resolver.resolver.lookup_ip(hostname.to_owned()).await {
 			| Err(e) => Self::handle_resolve_error(&e, hostname),
 			| Ok(override_ip) => {
-				self.cache.set_override(
-					untername,
-					&CachedOverride {
-						ips: override_ip.iter().take(MAX_IPS).collect(),
-						port,
-						expire: CachedOverride::default_expire(),
-						overriding: (hostname != untername)
-							.then_some(hostname.into())
-							.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
-					},
-				);
+				self.cache.set_override(untername, &CachedOverride {
+					ips: override_ip.into_iter().take(MAX_IPS).collect(),
+					port,
+					expire: CachedOverride::default_expire(),
+					overriding: (hostname != untername)
+						.then_some(hostname.into())
+						.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
+				});

 				Ok(())
 			},
@@ -415,7 +414,8 @@ fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> {
 		}
 	}

-	/// Ensure `dest` is a valid destination (valid ip if it is an IP), and not ourselves (unless in config)
+	/// Ensure `dest` is a valid destination (valid ip if it is an IP), and not
+	/// ourselves (unless in config)
 	fn validate_dest(&self, dest: &ServerName) -> Result<()> {
 		if dest == self.services.server.name && !self.services.server.config.federation_loopback {
 			return Err!("Won't send federation request to ourselves");
diff --git c/src/service/resolver/fed.rs i/src/service/resolver/fed.rs
index b43f62eed..83601a98a 100644
--- c/src/service/resolver/fed.rs
+++ i/src/service/resolver/fed.rs
@@ -9,8 +9,8 @@

 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
 pub enum FedDest {
-	Literal(SocketAddr),
-	Named(String, PortString),
+	Literal(SocketAddr),       // "ip:port"
+	Named(String, PortString), // ("hostname", ":port")
 }

 /// numeric or service-name
@@ -18,6 +18,9 @@ pub enum FedDest {

 const DEFAULT_PORT: &str = ":8448";

+/// Attempt to parse `dest_str` as either an IP:port socket pair or as a plain
+/// IP (adding the default port), returning `None` if dest_str is neither a
+/// socket pair nor a plain IP.
 pub(crate) fn get_ip_with_port(dest_str: &str) -> Option<FedDest> {
 	if let Ok(dest) = dest_str.parse::<SocketAddr>() {
 		Some(FedDest::Literal(dest))
@@ -28,6 +31,8 @@ pub(crate) fn get_ip_with_port(dest_str: &str) -> Option<FedDest> {
 	}
 }

+/// Convert a `dest` string with or without port into a FedDest with either
+/// the provided port (if host:port format) or the default port (8448)
 pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {
 	let (host, port) = match dest.find(':') {
 		| None => (dest, DEFAULT_PORT),
@@ -42,8 +47,8 @@ pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {

 /// Ensure `host` always has a port
 ///
-/// `get_ip_with_port` returns `None` if `host` isn't an IP:port string or plain IP,
-/// in which case `add_port_to_hostname` adds it instead
+/// `get_ip_with_port` returns `None` if `host` isn't an IP:port string or plain
+/// IP, in which case `add_port_to_hostname` adds it instead
 #[inline]
 pub(crate) fn ensure_host_has_port(host: &str) -> FedDest {
 	get_ip_with_port(host).unwrap_or_else(|| add_port_to_hostname(host))
2026-07-03 10:59:10 +02:00
theS1LV3R cb031aef84 refactor: Rewrite resolver service
No functional changes, only visual and slight logic updates.
All inputs should return the same outputs.

# Conflicts:
#	src/service/resolver/actual.rs

diff --git c/src/service/resolver/actual.rs i/src/service/resolver/actual.rs
index 7eaeb96ab..9cd1aec15 100644
--- c/src/service/resolver/actual.rs
+++ i/src/service/resolver/actual.rs
@@ -1,8 +1,9 @@
-use std::{
-	fmt::Debug,
-	net::{IpAddr, SocketAddr},
-};
+use std::fmt::Debug;

+use super::{
+	cache::{CachedDest, CachedOverride, MAX_IPS},
+	fed::{FedDest, PortString, add_port_to_hostname, ensure_host_has_port, get_ip_with_port},
+};
 use conduwuit::{Err, Result, debug, debug_info, err, error, trace};
 use futures::{FutureExt, TryFutureExt};
 use hickory_resolver::{
@@ -25,7 +26,9 @@ pub(crate) struct ActualDest {

 impl ActualDest {
 	#[inline]
-	pub(crate) fn string(&self) -> String { self.dest.https_string() }
+	pub(crate) fn string(&self) -> String {
+		self.dest.https_string()
+	}
 }

 impl super::Service {
@@ -57,71 +60,103 @@ pub(crate) async fn lookup_actual_dest(
 			.await
 	}

-	/// Returns: `actual_destination`, host header
-	/// Implemented according to the specification at <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
-	/// Numbers in comments below refer to bullet points in linked section of
-	/// specification
+	/// Returns: `actual_destination` + `host` variable used for logging
 	#[tracing::instrument(name = "actual", level = "debug", skip(self, cache))]
 	pub async fn resolve_actual_dest(
 		&self,
 		dest: &ServerName,
 		cache: bool,
 	) -> Result<CachedDest> {
+		debug!(
+			dest = %dest,
+			cache = %cache,
+			"Resolving server name and port"
+		);
+		// Ensure dest is a valid connection endpoint
 		self.validate_dest(dest)?;
-		let mut host = dest.as_str().to_owned();
-		let actual_dest = match get_ip_with_port(dest.as_str()) {
-			| Some(host_port) => Self::actual_dest_1(host_port)?,
-			| None =>
-				if let Some(pos) = dest.as_str().find(':') {
-					self.actual_dest_2(dest, cache, pos).await?
-				} else {
-					self.services.server.check_running()?;
-					match self.request_well_known(dest.as_str()).await? {
-						| Some(delegated) =>
-							self.actual_dest_3(&mut host, cache, delegated).await?,
-						| _ => match self.query_srv_record(dest.as_str()).await? {
-							| Some(overrider) =>
-								self.actual_dest_4(&host, cache, overrider).await?,
-							| _ => self.actual_dest_5(dest, cache).await?,
-						},
-					}
-				},
-		};

-		// Can't use get_ip_with_port here because we don't want to add a port
-		// to an IP address if it wasn't specified
-		let host = if let Ok(addr) = host.parse::<SocketAddr>() {
-			FedDest::Literal(addr)
-		} else if let Ok(addr) = host.parse::<IpAddr>() {
-			FedDest::Named(addr.to_string(), FedDest::default_port())
-		} else if let Some(pos) = host.find(':') {
-			let (host, port) = host.split_at(pos);
-			FedDest::Named(
-				host.to_owned(),
-				port.try_into().unwrap_or_else(|_| FedDest::default_port()),
-			)
-		} else {
-			FedDest::Named(host, FedDest::default_port())
-		};
+		// Clippy believes this can be a clone, however we are actually converting ServerName to String
+		#[allow(clippy::implicit_clone)]
+		let mut host = dest.to_string().to_owned();
+		let actual_dest = self.resolve_server_name(dest, cache, &mut host).await?;

-		debug!("Actual destination: {actual_dest:?} hostname: {host:?}");
+		host = ensure_host_has_port(&host).to_string();
+
+		debug!(
+			dest = %dest, // matrix.org
+			actual_dest = %actual_dest, // FedDest::Named(server.matrix.org, 443)
+			host = %host, // matrix.org
+			"Finished resolving server name"
+		);
 		Ok(CachedDest {
 			dest: actual_dest,
-			host: host.uri_string(),
+			host,
 			expire: CachedDest::default_expire(),
 		})
 	}

-	fn actual_dest_1(host_port: FedDest) -> Result<FedDest> {
-		debug!("1: IP literal with provided or default port");
-		Ok(host_port)
+	/// Performs the server resolution steps as per the specification:
+	/// <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
+	async fn resolve_server_name(
+		&self,
+		dest: &ServerName,
+		cache: bool,
+		host: &mut String,
+	) -> Result<FedDest> {
+		// 1. If `dest` is an IP, use it directly. If a port is provided as well (IP:port socket pair)
+		//    use that, otherwise default to port 8448
+		if let Some(fed_dest) = get_ip_with_port(dest.as_str()) {
+			debug!("1: IP literal with provided or default port");
+			return Ok(fed_dest);
+		}
+
+		// 2. If `dest` is a hostname and has a provided port (format of `host:port`),
+		//    resolve the hostname to an IP address and connect it and the provided port
+		if let Some(colon_position) = dest.as_str().find(':') {
+			self.resolve_2_host_port(dest, cache, colon_position)
+				.await?;
+		}
+
+		// Pre-resolve IP? Unsure what overrides exactly do, system is due to be removed either way
+		// https://matrix.to/#/!da26JtAjE6APGLnX8ncWsvc-skF2KQZ9Nw_MbNpYD2k/%24_hq6JP0JXANbMTMPdV64iZbgbsZdhy92M5ndDYGy6No
+		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, true)
+			.await?;
+
+		// Ensure server is running (not shutting down) before continuing resolution
+		self.services.server.check_running()?;
+
+		// 3. If `dest` is a hostname with no port, send GET to `https://<dest>/.well-known/matrix/server`.
+		// If invalid JSON (throws error), skip to step 4. Otherwise, parse `delegated` as `<hostname>[:<port>]` and...
+		if let Some(delegated) = self.request_well_known(dest.as_str()).await? {
+			// delegated=matrix-federation.matrix.org:443 // host=matrix.org
+			self.resolve_3_well_known(host, cache, delegated).await?;
+		}
+
+		// 4. if .well-known errored, perform SRV (see 3.3)
+		if let Some(overrider) = self.query_srv_record(dest.as_str()).await? {
+			self.resolve_4_srv_lookup(host, cache, overrider).await?;
+		}
+
+		// 5. if .well-known errored and no SRV exists, resolve IP and connect on default port (8448)
+		self.resolve_5_direct(dest, cache).await
 	}

-	async fn actual_dest_2(&self, dest: &ServerName, cache: bool, pos: usize) -> Result<FedDest> {
+	/// Parse a host:port socket pair into separate parts, and resolve the hostname into an IP address
+	async fn resolve_2_host_port(
+		&self,
+		dest: &ServerName,
+		cache: bool,
+		pos: usize,
+	) -> Result<FedDest> {
 		debug!("2: Hostname with included port");
 		let (host, port) = dest.as_str().split_at(pos);
-		self.conditional_query_and_cache(host, port.parse::<u16>().unwrap_or(8448), cache)
-			.await?;
+
+		self.conditional_query_and_cache(
+			host,
+			port.parse::<u16>().unwrap_or(DEFAULT_PORT),
+			cache,
+		)
+		.await?;

 		Ok(FedDest::Named(
 			host.to_owned(),
@@ -129,7 +164,7 @@ async fn actual_dest_2(&self, dest: &ServerName, cache: bool, pos: usize) -> Res
 		))
 	}

-	async fn actual_dest_3(
+	async fn resolve_3_well_known(
 		&self,
 		host: &mut String,
 		cache: bool,
@@ -137,63 +172,75 @@ async fn actual_dest_3(
 	) -> Result<FedDest> {
 		debug!("3: A .well-known file is available");
 		*host = add_port_to_hostname(&delegated).uri_string();
-		match get_ip_with_port(&delegated) {
-			| Some(host_and_port) => Self::actual_dest_3_1(host_and_port),
-			| None =>
-				if let Some(pos) = delegated.find(':') {
-					self.actual_dest_3_2(cache, delegated, pos).await
-				} else {
-					trace!("Delegated hostname has no port in this branch");
-					match self.query_srv_record(&delegated).await? {
-						| Some(overrider) =>
-							self.actual_dest_3_3(cache, delegated, overrider).await,
-						| _ => self.actual_dest_3_4(cache, delegated).await,
-					}
-				},
+
+		// 3.1 - If <delegated> is of IP:port format, connect to that,
+		//       or IP with default port if no port provided (8448)
+		if let Some(host_and_port) = get_ip_with_port(&delegated) {
+			debug!("3.1: IP with port in .well-known file");
+			return Ok(host_and_port);
 		}
+
+		// 3.2 - If <delegated> is not an IP and a port is present, lookup IP for hostname and connect
+		if let Some(pos) = &delegated.find(':') {
+			self.resolve_3_2_hostname_port(cache, &delegated, *pos)
+				.await?;
+		}
+
+		// 3.3 - If <delegated> is not an IP and there is no port, lookup SRV `_matrix._tcp.<delegated>`
+		// (which may provide a new hostname + port to use, see steps 3.1 and 3.2)
+		trace!("Delegated hostname has no port, querying SRV");
+		if let Some(overrider) = self.query_srv_record(&delegated).await? {
+			self.resolve_3_3_use_srv(cache, &delegated, overrider)
+				.await?;
+		}
+
+		self.resolve_3_4_use_default_port(cache, delegated).await
 	}

-	fn actual_dest_3_1(host_and_port: FedDest) -> Result<FedDest> {
-		debug!("3.1: IP literal in .well-known file");
-		Ok(host_and_port)
-	}
-
-	async fn actual_dest_3_2(
+	async fn resolve_3_2_hostname_port(
 		&self,
 		cache: bool,
-		delegated: String,
+		delegated: &str,
 		pos: usize,
 	) -> Result<FedDest> {
 		debug!("3.2: Hostname with port in .well-known file");
-		let (host, port) = delegated.split_at(pos);
-		self.conditional_query_and_cache(host, port.parse::<u16>().unwrap_or(8448), cache)
-			.await?;
+		let (host, port) = &delegated.split_at(pos);
+		self.conditional_query_and_cache(
+			host,
+			port.parse::<u16>().unwrap_or(DEFAULT_PORT),
+			cache,
+		)
+		.await?;

+		trace!("Successfully resolved IP for {delegated}");
 		Ok(FedDest::Named(
-			host.to_owned(),
-			port.try_into().unwrap_or_else(|_| FedDest::default_port()),
+			host.to_owned().to_owned(),
+			port.to_owned()
+				.try_into()
+				.unwrap_or_else(|_| FedDest::default_port()),
 		))
 	}

-	async fn actual_dest_3_3(
+	async fn resolve_3_3_use_srv(
 		&self,
 		cache: bool,
-		delegated: String,
+		delegated: &String,
 		overrider: FedDest,
 	) -> Result<FedDest> {
 		debug!("3.3: SRV lookup successful");
+
 		let force_port = overrider.port();
 		self.conditional_query_and_cache_override(
-			&delegated,
+			delegated,
 			&overrider.hostname(),
-			force_port.unwrap_or(8448),
+			force_port.unwrap_or(DEFAULT_PORT),
 			cache,
 		)
 		.await?;

 		if let Some(port) = force_port {
 			return Ok(FedDest::Named(
-				delegated,
+				delegated.to_owned(),
 				format!(":{port}")
 					.as_str()
 					.try_into()
@@ -201,17 +248,21 @@ async fn actual_dest_3_3(
 			));
 		}

-		Ok(add_port_to_hostname(&delegated))
+		Ok(add_port_to_hostname(delegated))
 	}

-	async fn actual_dest_3_4(&self, cache: bool, delegated: String) -> Result<FedDest> {
-		debug!("3.4: No SRV records, just use the hostname from .well-known");
-		self.conditional_query_and_cache(&delegated, 8448, cache)
+	async fn resolve_3_4_use_default_port(
+		&self,
+		cache: bool,
+		delegated: String,
+	) -> Result<FedDest> {
+		debug!("3.4: No SRV records found, use the hostname from .well-known with default port");
+		self.conditional_query_and_cache(&delegated, DEFAULT_PORT, cache)
 			.await?;
 		Ok(add_port_to_hostname(&delegated))
 	}

-	async fn actual_dest_4(
+	async fn resolve_4_srv_lookup(
 		&self,
 		host: &str,
 		cache: bool,
@@ -222,7 +273,7 @@ async fn actual_dest_4(
 		self.conditional_query_and_cache_override(
 			host,
 			&overrider.hostname(),
-			force_port.unwrap_or(8448),
+			force_port.unwrap_or(DEFAULT_PORT),
 			cache,
 		)
 		.await?;
@@ -239,9 +290,9 @@ async fn actual_dest_4(
 		Ok(add_port_to_hostname(host))
 	}

-	async fn actual_dest_5(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
-		debug!("5: No SRV record found");
-		self.conditional_query_and_cache(dest.as_str(), 8448, cache)
+	async fn resolve_5_direct(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
+		debug!("5: No port provided and no SRV record found");
+		self.conditional_query_and_cache(dest.as_str(), DEFAULT_PORT, cache)
 			.await?;

 		Ok(add_port_to_hostname(dest.as_str()))
@@ -261,9 +312,9 @@ async fn conditional_query_and_cache(
 	#[inline]
 	async fn conditional_query_and_cache_override(
 		&self,
-		untername: &str,
-		hostname: &str,
-		port: u16,
+		untername: &str, // matrix.org
+		hostname: &str,  // server.matrix.org
+		port: u16,       // 443
 		cache: bool,
 	) -> Result {
 		if !cache {
@@ -281,9 +332,9 @@ async fn conditional_query_and_cache_override(
 	#[tracing::instrument(name = "ip", level = "debug", skip(self))]
 	async fn query_and_cache_override(
 		&self,
-		untername: &'_ str,
-		hostname: &'_ str,
-		port: u16,
+		untername: &'_ str, // matrix.org
+		hostname: &'_ str,  // server.matrix.org
+		port: u16,          // 443
 	) -> Result {
 		self.services.server.check_running()?;

@@ -291,14 +342,17 @@ async fn query_and_cache_override(
 		match self.resolver.resolver.lookup_ip(hostname.to_owned()).await {
 			| Err(e) => Self::handle_resolve_error(&e, hostname),
 			| Ok(override_ip) => {
-				self.cache.set_override(untername, &CachedOverride {
-					ips: override_ip.iter().take(MAX_IPS).collect(),
-					port,
-					expire: CachedOverride::default_expire(),
-					overriding: (hostname != untername)
-						.then_some(hostname.into())
-						.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
-				});
+				self.cache.set_override(
+					untername,
+					&CachedOverride {
+						ips: override_ip.iter().take(MAX_IPS).collect(),
+						port,
+						expire: CachedOverride::default_expire(),
+						overriding: (hostname != untername)
+							.then_some(hostname.into())
+							.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
+					},
+				);

 				Ok(())
 			},
@@ -361,6 +415,7 @@ fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> {
 		}
 	}

+	/// Ensure `dest` is a valid destination (valid ip if it is an IP), and not ourselves (unless in config)
 	fn validate_dest(&self, dest: &ServerName) -> Result<()> {
 		if dest == self.services.server.name && !self.services.server.config.federation_loopback {
 			return Err!("Won't send federation request to ourselves");
@@ -370,6 +425,7 @@ fn validate_dest(&self, dest: &ServerName) -> Result<()> {
 			self.validate_dest_ip_literal(dest)?;
 		}

+		debug!(dest = %dest, "Valid destination for resolution");
 		Ok(())
 	}

diff --git c/src/service/resolver/fed.rs i/src/service/resolver/fed.rs
index e5bee9ac2..b43f62eed 100644
--- c/src/service/resolver/fed.rs
+++ i/src/service/resolver/fed.rs
@@ -40,6 +40,15 @@ pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {
 	)
 }

+/// Ensure `host` always has a port
+///
+/// `get_ip_with_port` returns `None` if `host` isn't an IP:port string or plain IP,
+/// in which case `add_port_to_hostname` adds it instead
+#[inline]
+pub(crate) fn ensure_host_has_port(host: &str) -> FedDest {
+	get_ip_with_port(host).unwrap_or_else(|| add_port_to_hostname(host))
+}
+
 impl FedDest {
 	pub(crate) fn https_string(&self) -> String {
 		match self {
2026-07-03 10:59:06 +02:00
Ginger f1b7710984 fix: Mobile styling improvements 2026-07-02 17:13:14 -04:00
Ginger 514c3bbcbf fix: Properly show email in account page when OIDC email_claim is set 2026-07-02 16:57:21 -04:00
Ginger da7d8c9821 fix: Prevent register page from being accessible when OIDC is enabled 2026-07-02 16:34:56 -04:00
Ginger 45809b7970 feat: Add support for reading the OIDC client secret from a file 2026-07-02 16:34:56 -04:00
Ginger 910925b906 fix: Config file formatting 2026-07-02 16:34:38 -04:00
Ginger 28263982cd feat: Add support for requesting additional scopes 2026-07-02 16:34:38 -04:00
Ginger 52a4b9d586 chore: News fragment 2026-07-02 16:34:38 -04:00
Ginger 786890baa0 feat: Add support for importing email addresses from the IDP 2026-07-02 16:34:38 -04:00
Ginger 3282ba8eab fix: Miscellaneous fixes 2026-07-02 16:34:38 -04:00
Ginger f66c1d8f35 fix: Hide bare oidc config option 2026-07-02 16:34:38 -04:00
Ginger 3d004b1923 fix: Adjust OIDC config section comment position 2026-07-02 16:34:38 -04:00
Ginger 06afe4f1a3 feat: Add support for importing profile data from claims 2026-07-02 16:34:38 -04:00
Ginger 53d6887b86 refactor: Move profile field setting logic into users service 2026-07-02 16:34:38 -04:00
Ginger 8c77bd61e5 fix: Hide password change link when OIDC is enabled 2026-07-02 16:34:38 -04:00
Ginger e649af33b9 feat: Send account selection prompt to IDP when account switch link is clicked 2026-07-02 16:34:38 -04:00
Ginger e2fe166d63 feat: Speedbump when logging in with OIDC with no next target 2026-07-02 16:34:38 -04:00
Ginger 4ee0bb7533 feat: Allow existing legacy accounts to be linked interactively 2026-07-02 16:34:38 -04:00
Ginger 9a9cc94702 chore: Update admin command docs 2026-07-02 16:34:38 -04:00
Ginger 58a6e1232c feat: Implement !admin oidc unlink 2026-07-02 16:34:38 -04:00
Ginger c0cb4afd8a feat: Initial implementation of OIDC 2026-07-02 16:34:38 -04:00
Ginger 69163b5391 refactor: Split remote and deactivated users into their own columns 2026-07-02 16:24:07 -04:00
Jade Ellis d4f6c053bc fix: Log the set password when creating users
Closes #1894
2026-07-02 16:41:33 +01:00
Renovate Bot 4f72326b07 chore(deps): update rust crate rand to v0.10.2 2026-07-02 10:19:35 +00:00
Renovate Bot b7e449f490 chore(deps): update actions/cache action to v6 2026-07-02 10:14:57 +00:00
renovate 2a1d7e49d9 chore(Nix): Updated flake hashes 2026-07-02 10:14:36 +00:00
Renovate Bot b7792f9e86 chore(deps): update rust to v1.96.1 2026-07-02 10:14:36 +00:00
Henry-Hiles adfe9f9f53 chore: disable doctests in conduwuit_core
These aren't wanted and are failing anyways. Fixes nix package build.
2026-07-01 23:39:28 -04:00
Renovate Bot 8fe1715019 chore(deps): update rust crate aws-lc-rs to v1.17.1 2026-07-01 19:46:32 +00:00
Renovate Bot 9a91dce600 chore(deps): update rust crate serde_html_form to v0.4.1 2026-07-01 19:45:14 +00:00
Jade Ellis eee4ef50d2 chore: merge 1818 nex/perf/get-missing-events 2026-07-01 18:45:08 +00:00
timedout 62e0b53f52 feat: Prioritise sending dummy events when the extremity count reaches 20 or more 2026-07-01 19:15:35 +01:00
timedout 5fa3d5f6c8 fix: Don't create fake objects when defaulting a partial PDU build 2026-07-01 19:15:35 +01:00
timedout 9b80a99aa7 style: De-duplicate build_local_dag definitions, replace outdated comment on unwrap safety 2026-07-01 19:15:22 +01:00
timedout bd525c100f style: Re-use room_id parameter instead of potentially recalculating 2026-07-01 16:31:59 +01:00
timedout e6225f3265 fix: Use infallible room ID getter 2026-07-01 16:13:55 +01:00
Charlotte 🦝 Deleńkec 54c94ca9ad fix(appservice): Re-add room membership conditions for event sending
They accidentally got removed in a recent refactor

Fixes #1890
2026-07-01 10:20:57 +01:00
timedout 19a83c6891 style: Add docstrings to squasher functions 2026-06-29 18:37:04 +01:00
timedout 2bfd678c71 feat: Add jitter to extremity squasher 2026-06-29 18:29:04 +01:00
timedout e936d18324 feat: Don't panic in build_local_dag
Previously the function assumed the caller had performed proper validation on the inputs (and all current callers do), but this is a poor reason to panic when sane error handling is available.
Events with no prev events now return an error, and prev events which are illegal are simply skipped.
2026-06-29 18:12:16 +01:00
timedout 90797fa3cd fix: Avoid string roundtrip when fetching timestamps in build_local_dag 2026-06-29 17:44:13 +01:00
Jade Ellis 5199cde870 feat: Allow sending dummy events to clients 2026-06-28 03:44:23 +01:00
Jade Ellis b6bc7dfc16 feat: Debounce extremity squashing
Additionaly circuit-breaks it if the squash would have only
b triggered by dummy events / other squashes
2026-06-28 01:29:52 +01:00
Jade Ellis 945ea5a78a chore: Box large futures 2026-06-28 01:29:52 +01:00
timedout d719fe2048 chore: Merge origin/main into nex/perf/get-missing-events 2026-06-28 01:29:23 +01:00
timedout 3b6858e936 perf: Throttle dummy events to prevent stampeding 2026-06-23 15:26:28 +01:00
timedout 7ca00e4ab9 chore: Drop unused param from handle_outlier_pdu 2026-06-20 17:08:00 +01:00
timedout b8ca06029f perf: Use a hashmap for full-state filtering 2026-06-20 17:08:00 +01:00
timedout 2efe8f2ec0 fix: Inverted power level check in extremity squash 2026-06-20 17:08:00 +01:00
timedout d1aa911739 fix: Rename variables in auth event fetcher
Also fixes a couple bugs where events were being misattributed
2026-06-20 17:08:00 +01:00
timedout 4673282ca1 perf: Don't try to fetch prevs we already fetched
graphs are hard
2026-06-20 17:08:00 +01:00
timedout 0c03195aec fix: Fall back to legacy behaviour when prev events are missed from get_missing_events 2026-06-20 17:08:00 +01:00
timedout 439bc2784d style: Use more explicit variable names 2026-06-20 17:08:00 +01:00
timedout 689a1ce59b style: Use user_can_send_message 2026-06-20 17:08:00 +01:00
timedout c141503ccb style: Re-use GET_MISSING_EVENTS_MAX_BATCH_SIZE 2026-06-20 17:08:00 +01:00
timedout bcadecdc3b feat: Add !admin debug rooms-by-extremity-count command 2026-06-20 17:08:00 +01:00
timedout ab3be337cb chore: Reformat 2026-06-20 17:08:00 +01:00
timedout aa1281a9e0 fix: Prevent arbitrary state injection attack 2026-06-20 17:08:00 +01:00
timedout 115a2e802e style: Check power levels before attempting to send extremity squashes
Solves a problem where the console screams in agony when local users can't send dummy events
2026-06-20 17:08:00 +01:00
timedout 968328d788 perf: Squash weird mutable variable 2026-06-20 17:08:00 +01:00
timedout f1cde5f323 style: Fix up some TODOs 2026-06-20 17:08:00 +01:00
timedout 832ee8650b style: Adjust docstrings and dodgy comment 2026-06-20 17:08:00 +01:00
timedout c0666a1793 fix: Default PDU content to empty object instead of literal NULL 2026-06-20 17:08:00 +01:00
timedout ddb4ef539f fix: un-forget how streams work 2026-06-20 17:08:00 +01:00
timedout 3faedc4581 perf: Remove huge clone and tackle TODOs 2026-06-20 17:08:00 +01:00
timedout a3544353ba feat: Automatically squash extremities when they exceed a threshold
Attempts to tackle #1844
2026-06-20 17:08:00 +01:00
timedout b0612397d3 style: Tidy up 2026-06-20 17:08:00 +01:00
timedout 31737e127e fix: Make fetch_state_ids_from_backfill_servers candidate-free safe 2026-06-20 17:08:00 +01:00
timedout 486dcd208c style: Resolve lint complaints 2026-06-20 17:08:00 +01:00
timedout a945a4b2ad fix: Correctly handle still-missing state, always fetch full state atomically if regular fetch fails 2026-06-20 17:08:00 +01:00
timedout c28ea44e11 fix: Correct inverted boolean condition, add explicit timeout on /state fetch 2026-06-20 17:08:00 +01:00
timedout 3e4d6b2565 perf: Always fetch at least N events per GME 2026-06-20 17:08:00 +01:00
timedout 29ce21cd2e fix: Correctly pre-populate state events vec with known events 2026-06-20 17:08:00 +01:00
timedout d461c6977a fix: Friendly assertations 2026-06-20 17:08:00 +01:00
timedout 5a0d6461d1 perf: Don't try to re-persist non-outliers we already have 2026-06-20 17:08:00 +01:00
timedout 2ec7394785 perf: Don't add trees we already have to latest boundary 2026-06-20 17:08:00 +01:00
timedout 8a5708b9f9 fix: Be noisy when there's no incoming state 2026-06-20 17:08:00 +01:00
timedout cd46070bd3 fix: Elide auth chain from fetch_and_handle_outliers 2026-06-20 17:08:00 +01:00
timedout 9930e549a0 fix: Progress log in fetch_prev 2026-06-20 17:08:00 +01:00
timedout 36ed20cb04 fix: Downgrade safe assert to debug assert 2026-06-20 17:08:00 +01:00
timedout 3907589b6c fix: Don't download the world 2026-06-20 17:08:00 +01:00
timedout 2a598af888 feat: Make logging more verbose to diagnose the aranjesplosion 2026-06-20 17:08:00 +01:00
timedout f34f84832b feat: Include timing information in debug logs 2026-06-20 17:08:00 +01:00
timedout 0efa6ed1f2 fix: Don't treat prev outlier upgrades as fetch failures 2026-06-20 17:08:00 +01:00
timedout 2bf5876778 fix: Ask more servers for state_ids when origin fails to provide
Some servers reference events in prev_events that they might not yet have finished processing, so this allows us to at least attempt to get the state from another trustworthy server in the room that might be faster. I don't think this is too effective, however it's more effective than giving up immediately.
2026-06-20 17:08:00 +01:00
timedout 31982e84de fix: Remove redundant check 2
This may look scary, but this is safe because event auth performs the same check, and will reject the event if it doesn't reference the create event correctly.
2026-06-20 17:08:00 +01:00
timedout 970958652a fix: Remove redundant check that accidentally banned everyone 2026-06-20 17:08:00 +01:00
timedout c94a395bf0 fix: Make PDU handle errors noisier & correct error types 2026-06-20 17:08:00 +01:00
timedout 43adff926f fix: Make dedupe noisy, don't allow non-create event as create event 2026-06-20 17:07:59 +01:00
timedout 04fce56381 fix: Don't silence PDU handle logs 2026-06-20 17:07:59 +01:00
timedout 4df2097e6c style: Rename gapfill helpers instruments 2026-06-20 17:07:59 +01:00
timedout 8b85b04d10 fix: Properly remove event_id from the PDU JSON before upgrading it 2026-06-20 17:07:59 +01:00
timedout 9e0bcd3be8 fix: Hold a federation room lock while remotely joining a room 2026-06-20 17:07:59 +01:00
timedout 9509080e0d fix: Replace our local extremity tracking when joining a disconnected room remotely 2026-06-20 17:07:59 +01:00
timedout 61066bb0c6 fix: Don't try and fetch zero events 2026-06-20 17:07:59 +01:00
timedout 573d5bc50e fix: Fall back to atomic fetch when full-state fetch fails 2026-06-20 17:07:59 +01:00
timedout 162e6eb92f fix: Remove short-term memory loss
I keep writing forgetful code, it's a problem
2026-06-20 17:07:59 +01:00
timedout cef4ebe38e fix: Don't try to fetch the same event endlessly 2026-06-20 17:07:59 +01:00
timedout 316a0b7d58 fix: Don't repeat already-included metadata in fetch_state instrument 2026-06-20 17:07:59 +01:00
timedout 2bcc56704b feat: Enhance reliability by fetching full state when we're missing a lot of auth events 2026-06-20 17:07:59 +01:00
timedout 7f64de9727 fix: Calculate max iterations dynamically, and bump max prevs 2026-06-20 17:07:59 +01:00
timedout aea03f2f99 perf(wip): Improve individual events fetcher 2026-06-20 17:07:59 +01:00
timedout 8edf9552b8 fix: Don't lie about using already-known content 2026-06-20 17:07:59 +01:00
timedout d5f69c8a31 fix: Be smarter when re-receiving already-seen PDUs 2026-06-20 17:07:59 +01:00
timedout 9ea9b0e04c perf: Don't re-process events as outliers 2026-06-20 17:07:59 +01:00
timedout e8db01fc8d style: Improve logging 2026-06-20 17:07:59 +01:00
timedout f80e1e89a5 fix: Lower floor for min depth 2026-06-20 17:07:59 +01:00
timedout b9dca84acf fix: Only increment mindepth on state events 2026-06-20 17:07:59 +01:00
timedout e3ec1066c4 chore: Add newsfrag 2026-06-20 17:07:59 +01:00
timedout 1445a8d446 feat: Keep track of a min_depth value
Should prevent weird situations where we accidentally gapfill into backfill territory
2026-06-20 17:07:59 +01:00
timedout 9547c438d6 perf: Increase default max_fetch_prev_events to 256 2026-06-20 17:07:59 +01:00
timedout 51d0e615f5 perf: Make max gap depth fetch configurable 2026-06-20 17:07:59 +01:00
timedout eeb937416c perf: Improve gap filling, handle missing auth events better 2026-06-20 17:07:59 +01:00
timedout 0d5aa7ede1 fix: This is some bullshit I tell you 2026-06-20 17:07:59 +01:00
timedout ba9dc27773 feat: Better prev event fetching
fix: Don't panic in debug mode when making an empty notary query
2026-06-20 17:07:59 +01:00
timedout abf5a155ba feat: Add backfill_missing_events helper 2026-06-20 17:07:59 +01:00
138 changed files with 4929 additions and 2691 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ runs:
echo "version=$(rustup --version)" >> $GITHUB_OUTPUT
- name: Cache rustup toolchains
if: steps.rustup-version.outputs.version == ''
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.rustup
@@ -57,7 +57,7 @@ runs:
- name: Check for LLVM cache
id: cache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/usr/bin/clang-*
+2 -2
View File
@@ -65,7 +65,7 @@ runs:
- name: Cache toolchain binaries
id: toolchain-cache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
.cargo/bin
@@ -76,7 +76,7 @@ runs:
- name: Cache Cargo registry and git
id: registry-cache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
.cargo/registry/index
+4 -4
View File
@@ -31,7 +31,7 @@ runs:
- name: Restore binary cache
id: binary-cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/usr/share/rust/.cargo/bin
@@ -77,7 +77,7 @@ runs:
- name: Save binary cache
if: steps.check-binaries.outputs.need-install == 'true'
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/usr/share/rust/.cargo/bin
@@ -87,7 +87,7 @@ runs:
- name: Restore timelord cache with fallbacks
id: timelord-restore
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ env.TIMELORD_CACHE_PATH }}
key: ${{ env.TIMELORD_KEY }}
@@ -114,7 +114,7 @@ runs:
timelord sync --source-dir ${{ env.TIMELORD_PATH }} --cache-dir ${{ env.TIMELORD_CACHE_PATH }}
- name: Save updated timelord cache immediately
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ env.TIMELORD_CACHE_PATH }}
key: ${{ env.TIMELORD_KEY }}
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
ref: ${{ github.ref_name }}
- name: Cache Cargo registry
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/registry
+3 -3
View File
@@ -37,7 +37,7 @@ jobs:
- name: Cache DNF packages
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/var/cache/dnf
@@ -47,7 +47,7 @@ jobs:
dnf-fedora${{ steps.fedora.outputs.version }}-
- name: Cache Cargo registry
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/registry
@@ -57,7 +57,7 @@ jobs:
cargo-fedora${{ steps.fedora.outputs.version }}-
- name: Cache Rust build dependencies
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/rpmbuild/BUILD/*/target/release/deps
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
node-version: 22
- name: Cache npm dependencies
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.npm
key: continuwuity-rspress-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}-${{ hashFiles('package-lock.json') }}
+5 -5
View File
@@ -55,7 +55,7 @@ jobs:
run: /usr/local/renovate/node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
- name: Restore renovate repo cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/tmp/renovate/cache/renovate/repository
@@ -64,7 +64,7 @@ jobs:
renovate-repo-cache-
- name: Restore renovate package cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/tmp/renovate/cache/renovate/renovate-cache-sqlite
@@ -73,7 +73,7 @@ jobs:
renovate-package-cache-
- name: Restore renovate OSV cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/tmp/osv
@@ -117,7 +117,7 @@ jobs:
- name: Save renovate package cache
if: always()
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/tmp/renovate/cache/renovate/renovate-cache-sqlite
@@ -125,7 +125,7 @@ jobs:
- name: Save renovate OSV cache
if: always()
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
/tmp/osv
Generated
+698 -288
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -141,6 +141,12 @@ features = [
version = "0.23.25"
default-features = false
[workspace.dependencies.aws-lc-sys]
version = "0.41.0"
[workspace.dependencies.aws-lc-rs]
version = "1.17.0"
[workspace.dependencies.reqwest]
version = "0.13.2"
default-features = false
@@ -403,6 +409,9 @@ default-features = false
version = "0.11.0"
default-features = false
[workspace.dependencies.openidconnect]
version = "4.0.1"
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
[workspace.dependencies.opentelemetry]
version = "0.32.0"
@@ -560,6 +569,9 @@ features = ["std"]
[workspace.dependencies.nonzero_ext]
version = "0.3.0"
[workspace.dependencies.resolvematrix]
version = "0.1.0"
[workspace.dependencies.serde_urlencoded]
version = "0.7.1"
+1
View File
@@ -0,0 +1 @@
Improved invite and join reliability in clients using legacy sync. Contributed by @ginger
+1
View File
@@ -0,0 +1 @@
Rewrite the resolver service to use [resolvematrix](https://forgejo.ellis.link/continuwuation/resolvematrix) for server resolution. Rewrite by @s1lv3r, crate by @Jade
+2
View File
@@ -0,0 +1,2 @@
Improved the performance and reliability of fetching missing events, improving network partition recovery. Contributed
by @nex.
+1
View File
@@ -0,0 +1 @@
Added support for linking an external identity provider with OIDC. Contributed by @ginger.
+107 -1
View File
@@ -297,7 +297,7 @@
# This item is undocumented. Please contribute documentation for it.
#
#max_fetch_prev_events = 192
#max_fetch_prev_events = 1024
# How many incoming federation transactions the server is willing to be
# processing at any given time before it becomes overloaded and starts
@@ -643,6 +643,14 @@
#
#default_room_acl_deny =
# The number of forward extremities to tolerate in a room before
# attempting to manually squash them with a "dummy event". Setting this
# above 20 will hinder its efficacy, and setting it below 5 will cause
# more dummy events to be sent than necessary (which increases federation
# traffic).
#
#dummy_event_threshold = 10
# Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
# Jaeger exporter. Traces will be sent via OTLP to a collector (such as
# Jaeger) that supports the OpenTelemetry Protocol.
@@ -1426,6 +1434,11 @@
#
#send_messages_from_ignored_users_to_client = false
# Send "org.matrix.dummy_event" events to the client. This is a debugging
# option.
#
#send_dummy_events_to_clients = false
# Vector list of IPv4 and IPv6 CIDR ranges / subnets *in quotes* that you
# do not want continuwuity to send outbound requests to. Defaults to
# RFC1918, unroutable, loopback, multicast, and testnet addresses for
@@ -2015,3 +2028,96 @@
# legacy authentication will be unable to log in.
#
#compatibility_mode = "hybrid"
#[global.oauth.oidc]
# Uncommenting this section will enable Continuwuity's support for
# authenticating users using an OpenID Connect-compatible identity provider.
# This is referred to as "delegated authentication".
#
# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if
# the `global.oauth.compatibility_mode` setting is set to `exclusive`.
# Matrix clients which do not support OAuth login (also referred to as "next-gen auth") will NOT be able
# to log in while delegated authentication is active.
# The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to
# automatically fetch the identity provider's metadata from this URL.
# Generally you should set this to the base domain your identity provider
# runs on.
#
#discovery_url =
# The OAuth client ID for Continuwuity to use when communicating with the
# identity provider.
#
#client_id =
# The OAuth client secret for Continuwuity to use when communicating with
# the identity provider.
#
#client_secret =
# A path to a file which Continuwuity will read the client secret from.
# If this option is set, it will override `client_secret`.
#
# The server will fail to start if the file cannot be read.
#
#client_secret_file =
# Additional scopes Continuwuity should request from the IDP. This may be
# necessary to access certain claims. Continuwuity always requests the
# `openid` scope.
#
#additional_scopes = []
# Whether the user should be prompted to choose a localpart
# when signing in for the first time. If this is `false`, Continuwuity
# will attempt to use the value of the `preferred_username_claim`
# (see below) as the user's localpart. Authentication will
# fail if this claim is missing or is not a valid localpart.
#
#prompt_for_localpart = true
# The claim to use for the user's localpart, if `prompt_for_localpart` is
# false.
#
#preferred_username_claim = "preferred_username"
# The claim which will be used to set the user's email address,
# either on initial registration or on every login depending on
# the value of `profile_key_import_mode`. Continuwuity assumes that
# the IDP has taken care of verifying that the user controls the email
# address it provides.
#
# This option does nothing if SMTP is not configured.
#
# If this option is set, and `profile_key_import_mode` is `on_login`,
# users will not be able to change their email addresses themselves.
#
#email_claim = "email"
# Defines how claims returned from the IDP should be mapped to a user's
# profile data. The profile field named in each key will be set from the
# claim named in the corresponding value when the user first registers,
# and possibly on subsequent logins as well, depending on the value of
# `profile_key_import_mode` (see below).
#
# Per-room overrides to the user's display name or avatar will be
# preserved by the import process.
#
# SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will
# perform a HTTP GET to the URL in the mapped claim and use the returned
# file as the user's profile picture. Make sure your users are not able
# to set the value of the mapped claim to an arbitrary URL.
#
#profile_key_map = { displayname = "name" }
# When profile keys should be imported from the IDP's claims.
#
# - "on_registration": Listed keys will be imported once, when the user
# logs in for the first time and their shadow account is created.
# - "on_login": Listed keys will be imported every time the user logs in.
# Additionally, users will not be able to manually edit any listed keys
# through their Matrix client.
#
#profile_key_import_mode = "on_registration"
+13 -1
View File
@@ -10,7 +10,13 @@ ## `!admin debug echo`
## `!admin debug get-auth-chain`
Get the auth_chain of a PDU
Loads the auth_chain of a PDU, reporting how long it took
## `!admin debug show-auth-chain`
Walks & displays the auth_chain of a PDU in a mermaid graph format.
This is useless to basically anyone but developers, and is also probably slow and memory hungry.
## `!admin debug parse-pdu`
@@ -44,6 +50,12 @@ ## `!admin debug get-room-state`
Of course the check is still done on the actual client API.
## `!admin debug get-state-at`
Gets all the room state events at the specified event.
State at event might not be available for some PDUs, such as rejected ones.
## `!admin debug get-signing-keys`
Get and display signing keys from local cache or remote server
+1
View File
@@ -14,6 +14,7 @@ ## Categories
- [`!admin appservices`](appservices/): Commands for managing appservices
- [`!admin users`](users/): Commands for managing local users
- [`!admin token`](token/): Commands for managing registration tokens
- [`!admin oidc`](oidc/): Commands for managing OIDC
- [`!admin rooms`](rooms/): Commands for managing rooms
- [`!admin federation`](federation/): Commands for managing federation
- [`!admin server`](server/): Commands for managing the server
+13
View File
@@ -0,0 +1,13 @@
<!-- This file is generated by `cargo xtask generate-docs`. Do not edit. -->
# `!admin oidc`
Commands for managing OIDC
## `!admin oidc link`
Link a user ID to the given subject claim
## `!admin oidc unlink`
Unlink the given subject claim from its associated user ID
+8 -4
View File
@@ -12,10 +12,6 @@ ## `!admin users reset-password`
Reset user password
## `!admin users issue-password-reset-link`
Issue a self-service password reset link for a user
## `!admin users get-email`
Get a user's associated email address
@@ -96,6 +92,14 @@ ## `!admin users list-users`
List local users in the database
## `!admin users list-invited-rooms`
Lists all the rooms (local and remote) that the specified user is invited to
## `!admin users reject-all-invites`
Manually make a user reject all current invites
## `!admin users list-joined-rooms`
Lists all the rooms (local and remote) that the specified user is joined in
+1 -1
View File
@@ -12,7 +12,7 @@
target:
target.fromToolchainName {
name = (lib.importTOML "${inputs.self}/rust-toolchain.toml").toolchain.channel;
sha256 = "sha256-mvUGEOHYJpn3ikC5hckneuGixaC+yGrkMM/liDIDgoU=";
sha256 = "sha256-h+t2xTBz5yt2YIO+1VMIIGlCU7gyp2LYOFvaV1nwOXU=";
};
in
{
+25 -25
View File
@@ -383,9 +383,9 @@
}
},
"node_modules/@rspress/core": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.15.tgz",
"integrity": "sha512-epLmUXYscNRw/GtQZx2oknoBE9wKbCrUGEOrQEDI4Qq8X32GdM4d7itzuHsliY7q3IbffKx8rMVbvlmygEocTQ==",
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.16.tgz",
"integrity": "sha512-jJcYNNBKY/VLR8oxLqdd5uvm6bHahXeF3wSaoAe2U1hxWWwoP9k4rBTDx9X3JkUWcgnthu7UgtMiHeLs+2fhFg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -393,7 +393,7 @@
"@mdx-js/react": "^3.1.1",
"@rsbuild/core": "^2.0.15",
"@rsbuild/plugin-react": "~2.0.1",
"@rspress/shared": "2.0.15",
"@rspress/shared": "2.0.16",
"@shikijs/rehype": "^4.2.0",
"@types/unist": "^3.0.3",
"@unhead/react": "^2.1.15",
@@ -412,11 +412,11 @@
"react-lazy-with-preload": "^2.2.1",
"react-reconciler": "0.33.0",
"react-render-to-markdown": "19.1.0",
"react-router-dom": "^7.15.1",
"react-router-dom": "^7.18.1",
"rehype-external-links": "^3.0.0",
"rehype-raw": "^7.0.0",
"remark-cjk-friendly": "^2.3.1",
"remark-cjk-friendly-gfm-strikethrough": "^2.1.0",
"remark-cjk-friendly-gfm-strikethrough": "^2.3.1",
"remark-gfm": "^4.0.1",
"remark-mdx": "^3.1.1",
"remark-parse": "^11.0.0",
@@ -436,9 +436,9 @@
}
},
"node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.15.tgz",
"integrity": "sha512-bPf/KIHH7Y6huLTtK6JXwRfxM7zKjksoxm46+IBsF1wisw0doKkEKR9HwJydxWnykyKBbA2cuZOaoT4h174Z1w==",
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.16.tgz",
"integrity": "sha512-FEjZb+3lxpkEESdt0uWa4dRQU7d/Okn5SyX7CDaMHdHuqg4XdOidAQ/95ZzgXomN7YVPv40eVe/0is3oWnAjew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -449,9 +449,9 @@
}
},
"node_modules/@rspress/plugin-sitemap": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.15.tgz",
"integrity": "sha512-z1hbyGP79ZXdSGJxiWw7ZjmX8qW0q9nXMDxr14cVEg/wdj7ToVzGtZHw0wvTPE0YiKG3BMiGkVNfE1rdOaPXiQ==",
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.16.tgz",
"integrity": "sha512-zRPvKAGF8EexblJvrkhhHtD2Kqlbaw6XFjbIMM07gs0SIUKNg1o3T2I/uo5cvWtk0pGfYyyAJj94HoqLUgUsEw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -462,9 +462,9 @@
}
},
"node_modules/@rspress/shared": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.15.tgz",
"integrity": "sha512-o8aYwEzNuTmWnmKe91ntPv+34u3RbtAe+rcK9XC5MANOlgncwOaCs3bUa8/B1/llwyLoNgrpi+VB9bEiU11ZRQ==",
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.16.tgz",
"integrity": "sha512-FjBSfGtgrlR1bRJ0EQLyNo2qXUXxzb2QE3NPRIICf8TKpP413gRCMyMRtzhbIqD4Gn7k+em82VAkWTAAQjQLTw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2869,9 +2869,9 @@
}
},
"node_modules/react-router": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
"integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2892,13 +2892,13 @@
}
},
"node_modules/react-router-dom": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
"integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-router": "7.17.0"
"react-router": "7.18.1"
},
"engines": {
"node": ">=20.0.0"
@@ -3081,9 +3081,9 @@
}
},
"node_modules/remark-cjk-friendly-gfm-strikethrough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.1.0.tgz",
"integrity": "sha512-3Kyq2hjY7V7eU8MbVbWW6QQLN81pjJcIvKHvPxr8hZZmcq/9wqm3MJ3iUG34Ch9QTM4WHN+a1JVAVC1fSi5mig==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.3.1.tgz",
"integrity": "sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -10,7 +10,7 @@
[toolchain]
profile = "minimal"
channel = "1.96.0"
channel = "1.96.1"
components = [
# For rust-analyzer
"rust-src",
+1
View File
@@ -92,6 +92,7 @@ serde-saphyr.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
resolvematrix.workspace = true
[lints]
workspace = true
+25 -10
View File
@@ -1,5 +1,5 @@
use clap::Parser;
use conduwuit::Result;
use conduwuit::{Err, Result};
use crate::{
appservice::{self, AppserviceCommand},
@@ -8,6 +8,7 @@
debug::{self, DebugCommand},
federation::{self, FederationCommand},
media::{self, MediaCommand},
oidc::{self, OidcCommand},
query::{self, QueryCommand},
room::{self, RoomCommand},
server::{self, ServerCommand},
@@ -18,44 +19,48 @@
#[derive(Debug, Parser)]
#[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())]
pub enum AdminCommand {
#[command(subcommand)]
/// Commands for managing appservices
#[command(subcommand)]
Appservices(AppserviceCommand),
#[command(subcommand)]
/// Commands for managing local users
#[command(subcommand)]
Users(UserCommand),
#[command(subcommand)]
/// Commands for managing registration tokens
#[command(subcommand)]
Token(TokenCommand),
/// Commands for managing OIDC
#[command(subcommand)]
Oidc(OidcCommand),
/// Commands for managing rooms
#[command(subcommand)]
Rooms(RoomCommand),
#[command(subcommand)]
/// Commands for managing federation
#[command(subcommand)]
Federation(FederationCommand),
#[command(subcommand)]
/// Commands for managing the server
#[command(subcommand)]
Server(ServerCommand),
#[command(subcommand)]
/// Commands for managing media
#[command(subcommand)]
Media(MediaCommand),
#[command(subcommand)]
/// Commands for checking integrity
#[command(subcommand)]
Check(CheckCommand),
#[command(subcommand)]
/// Commands for debugging things
#[command(subcommand)]
Debug(DebugCommand),
#[command(subcommand)]
/// Low-level queries for database getters and iterators
#[command(subcommand)]
Query(QueryCommand),
}
@@ -80,6 +85,16 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
context.bail_restricted()?;
token::process(command, context).await
},
| Oidc(command) => {
// OIDC commands are all restricted
context.bail_restricted()?;
if !context.services.oidc.enabled() {
return Err!("OIDC is not configured");
}
oidc::process(command, context).await
},
| Rooms(command) => room::process(command, context).await,
| Federation(command) => federation::process(command, context).await,
| Server(command) => server::process(command, context).await,
+6 -1
View File
@@ -6,7 +6,12 @@
impl Context<'_> {
pub(super) async fn check_all_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let users = self.services.users.stream().collect::<Vec<_>>().await;
let users = self
.services
.users
.stream_local_users()
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
let total = users.len();
+75 -7
View File
@@ -20,6 +20,10 @@
};
use futures::{FutureExt, StreamExt, TryStreamExt};
use lettre::message::Mailbox;
use resolvematrix::{
resolution::ResolvedDestination,
server::{MatrixResolver, MatrixResolverBuilder},
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, UInt,
@@ -31,6 +35,8 @@
};
use tracing_subscriber::EnvFilter;
use crate::PAGE_SIZE;
#[derive(Clone, Copy, Eq, PartialEq)]
enum NodeStatus {
Normal(bool),
@@ -610,7 +616,7 @@ pub(super) async fn force_device_list_updates(&self) -> Result {
// Force E2EE device list updates for all users
self.services
.users
.stream()
.stream_local_users()
.for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await)
.await;
@@ -1005,13 +1011,23 @@ pub(super) async fn resolve_true_destination(
);
}
let actual = self
.services
.resolver
.resolve_actual_dest(&server_name, !no_cache)
.await?;
let resolver: &MatrixResolver = if no_cache {
&MatrixResolverBuilder::new()
.dangerous_tls_accept_invalid_certs(self.services.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
.http_client(self.services.client.default.clone())
.build()?
} else {
&self.services.resolver.resolver
};
let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host);
let actual = resolver.resolve_server(server_name.as_str()).await?;
let destination = match actual.destination {
| ResolvedDestination::Literal(addr) => addr.to_string(),
| ResolvedDestination::Named(host, port) => format!("{host}:{port}"),
};
let msg = format!("Destination: {}\nHostname URI (SNI): {}", destination, actual.host);
self.write_str(&msg).await
}
@@ -1166,4 +1182,56 @@ pub(super) async fn send_test_email(&self) -> Result {
Ok(())
}
pub(super) async fn rooms_by_extremity_count(&self, page: Option<usize>) -> Result {
let page = page.unwrap_or(1);
// My Giant Chain:tm:
let mapped: HashMap<OwnedRoomId, u64> = self
.services
.rooms
.state
.all_forward_extremities()
.ready_fold(HashMap::new(), move |mut map, (room_id, _)| {
let count: u64 = map.get(&room_id).copied().unwrap_or(0);
map.insert(room_id, count.saturating_add(1));
map
})
.await
.into_iter()
.filter_map(|(room_id, count)| (count >= 2).then_some((room_id, count)))
.collect();
if mapped.is_empty() {
return Err!("No more rooms.");
}
let mut rooms = mapped.keys().collect::<Vec<_>>();
rooms.sort_by_key(|room_id| {
mapped
.get(*room_id)
.copied()
.expect("keys must have values")
});
rooms.reverse();
let body = rooms
.into_iter()
.stream()
.skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE))
.take(PAGE_SIZE)
.map(|room_id| {
format!(
"{room_id}: {}",
mapped.get(room_id).copied().expect("keys must have values")
)
})
.collect::<Vec<_>>()
.await;
self.write_str(&format!(
"Rooms by extremity count ({}):\n```\n{}\n```",
body.len(),
body.join("\n")
))
.await
}
}
+5
View File
@@ -245,6 +245,11 @@ pub enum DebugCommand {
/// Send a test email to the invoking admin's email address
SendTestEmail,
/// Lists room IDs by forward extremity count in descending order
RoomsByExtremityCount {
page: Option<usize>,
},
/// Developer test stubs
#[command(subcommand)]
#[allow(non_snake_case)]
+1
View File
@@ -16,6 +16,7 @@
pub(crate) mod debug;
pub(crate) mod federation;
pub(crate) mod media;
pub(crate) mod oidc;
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
+25
View File
@@ -0,0 +1,25 @@
use conduwuit::Result;
use crate::utils::parse_active_local_user_id;
impl crate::Context<'_> {
pub(super) async fn oidc_link(&self, user_id: String, subject: String) -> Result {
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
self.services.oidc.link_user(&user_id, &subject);
self.write_str(&format!("Subject `{subject}` linked to account `{user_id}`."))
.await?;
Ok(())
}
pub(super) async fn oidc_unlink(&self, subject: String) -> Result {
self.services.oidc.unlink_user(&subject);
self.write_str(&format!("Subject `{subject}` unlinked."))
.await?;
Ok(())
}
}
+22
View File
@@ -0,0 +1,22 @@
mod commands;
use clap::Subcommand;
use conduwuit::Result;
use conduwuit_macros::admin_command_dispatch;
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum OidcCommand {
/// Link a user ID to the given subject claim.
#[clap(name = "link")]
OidcLink {
user_id: String,
subject: String,
},
/// Unlink the given subject claim from its associated user ID.
#[clap(name = "unlink")]
OidcUnlink {
subject: String,
},
}
+10 -5
View File
@@ -46,7 +46,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;
let mut destinations = self.services.resolver.cache.destinations().boxed();
let mut destinations = self.services.resolver.dns.cache.destinations().boxed();
while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
if let Some(server_name) = server_name.as_ref() {
@@ -69,7 +69,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?;
writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;
let mut overrides = self.services.resolver.cache.overrides().boxed();
let mut overrides = self.services.resolver.dns.cache.overrides().boxed();
while let Some((name, CachedOverride { ips, port, expire, overriding })) =
overrides.next().await
@@ -92,11 +92,16 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
if all {
self.services.resolver.cache.clear().await;
self.services.resolver.resolver.clear_cache();
self.services.resolver.dns.cache.clear().await;
writeln!(self, "Resolver caches cleared!").await
} else if let Some(name) = name {
self.services.resolver.cache.del_destination(&name);
self.services.resolver.cache.del_override(&name);
self.services
.resolver
.resolver
.remove_cache_entry(name.as_str());
self.services.resolver.dns.cache.del_destination(&name);
self.services.resolver.dns.cache.del_override(&name);
self.write_str(&format!("Cleared {name} from resolver caches!"))
.await
} else {
+8 -3
View File
@@ -191,8 +191,13 @@ async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result {
async fn iter_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let result: Vec<OwnedUserId> =
self.services.users.stream().map(Into::into).collect().await;
let result: Vec<OwnedUserId> = self
.services
.users
.stream_local_users()
.map(Into::into)
.collect()
.await;
let query_time = timer.elapsed();
@@ -202,7 +207,7 @@ async fn iter_users(&self) -> Result {
async fn iter_users2(&self) -> Result {
let timer = tokio::time::Instant::now();
let result: Vec<_> = self.services.users.stream().collect().await;
let result: Vec<_> = self.services.users.stream_local_users().collect().await;
let result: Vec<_> = result
.into_iter()
.map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned())
+1 -1
View File
@@ -44,7 +44,7 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
.services
.config
.oauth
.compatibility_mode
.compatibility_mode()
.oauth_available()
{
self.write_str(&format!(
+53 -63
View File
@@ -20,7 +20,7 @@
tag::{TagEvent, TagEventContent, TagInfo},
},
};
use service::users::HashedPassword;
use service::users::{AccountStatus, HashedPassword};
use crate::{
get_room_info,
@@ -54,16 +54,16 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
.determine_registration_user_id(Some(username), None, None)
.await?;
let password = HashedPassword::new(
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)),
)?;
let password =
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
self.services
.users
.create_local_account(&user_id, password, None)
.await;
.create_local_account(&user_id, Some(HashedPassword::new(password)?), None)
.await?;
self.write_str(&format!("Created user {user_id}")).await
self.write_str(&format!("Created user {user_id} with password `{password}`"))
.await
}
pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result {
@@ -103,15 +103,12 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
pub(super) async fn suspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to suspend the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot be suspended.");
}
@@ -127,15 +124,12 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to unsuspend the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
self.services.users.unsuspend_account(&user_id).await;
self.write_str(&format!("User {user_id} has been unsuspended."))
@@ -147,6 +141,7 @@ pub(super) async fn reset_password(
logout: bool,
username: String,
password: Option<String>,
convert_to_local_account: bool,
) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
@@ -159,15 +154,37 @@ pub(super) async fn reset_password(
let new_password =
password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
let new_password_hash = HashedPassword::new(&new_password)?;
self.services
.users
.set_password(&user_id, Some(HashedPassword::new(&new_password)?));
if convert_to_local_account {
self.services
.users
.convert_to_local_account(&user_id, new_password_hash)
.await?;
} else {
match self.services.users.status(&user_id).await {
| AccountStatus::Active if !self.services.users.is_shadow(&user_id).await => {
self.services
.users
.set_password(&user_id, new_password_hash)
.await?;
},
| AccountStatus::NotFound => {
return Err!("The provided user does not exist.");
},
| _ => {
return Err!(
"The provided user is a shadow or deactivated account. To convert it to \
a local account, pass the --convert-to-local-account flag."
);
},
}
self.write_str(&format!(
"Successfully reset the password for user {user_id}: `{new_password}`"
))
.await?;
self.write_str(&format!(
"Successfully reset the password for user {user_id}: `{new_password}`"
))
.await?;
}
if logout {
self.services
@@ -919,21 +936,16 @@ pub(super) async fn force_leave_remote_room(
pub(super) async fn lock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to lock the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot be locked.");
}
self.services
.users
.lock_account(&user_id, self.sender_or_service_user())
@@ -945,11 +957,8 @@ pub(super) async fn lock(&self, user_id: String) -> Result {
pub(super) async fn unlock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
self.services.users.unlock_account(&user_id).await;
self.write_str(&format!("User {user_id} has been unlocked."))
@@ -958,21 +967,16 @@ pub(super) async fn unlock(&self, user_id: String) -> Result {
pub(super) async fn logout(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to log out the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("You cannot forcefully log out admin users.");
}
self.services
.users
.all_device_ids(&user_id)
@@ -989,18 +993,12 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
pub(super) async fn disable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to disable login for the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot have their login disallowed.");
}
@@ -1014,14 +1012,8 @@ pub(super) async fn disable_login(&self, user_id: String) -> Result {
pub(super) async fn enable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
self.services.users.enable_login(&user_id);
self.write_str(&format!("{user_id} can now log in.")).await
@@ -1129,10 +1121,8 @@ pub(super) async fn change_email(&self, user_id: String, email: Option<String>)
}
pub(super) async fn reset_push_rules(&self, user_id: String) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
if !self.services.users.is_active(&user_id).await {
return Err!("User is not active.");
}
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
recreate_push_rules_and_return(self.services, &user_id).await?;
self.write_str("Reset user's push rules to the server default.")
.await
+3
View File
@@ -27,6 +27,9 @@ pub enum UserCommand {
username: String,
/// New password for the user, if unspecified one is generated
password: Option<String>,
#[arg(long)]
convert_to_local_account: bool,
},
/// Get a user's associated email address.
+1 -8
View File
@@ -54,14 +54,7 @@ pub(crate) async fn parse_active_local_user_id(
user_id: &str,
) -> Result<OwnedUserId> {
let user_id = parse_local_user_id(services, user_id)?;
if !services.users.exists(&user_id).await {
return Err!("User {user_id:?} does not exist on this server.");
}
if services.users.is_deactivated(&user_id).await? {
return Err!("User {user_id:?} is deactivated.");
}
services.users.status(&user_id).await.ensure_active()?;
Ok(user_id)
}
+2 -1
View File
@@ -128,7 +128,8 @@ pub(crate) async fn change_password_route(
services
.users
.set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?));
.set_password(&sender_user, HashedPassword::new(&body.new_password)?)
.await?;
if body.logout_devices {
// Logout all devices except the current one
+4 -4
View File
@@ -72,7 +72,7 @@ pub(crate) async fn register_route(
.determine_registration_user_id(body.username.clone(), None, Some(appservice_info))
.await?;
services.users.create(&user_id, None)?;
services.users.create_shadow_account(&user_id).await?;
user_id
} else {
@@ -97,8 +97,8 @@ pub(crate) async fn register_route(
services
.users
.create_local_account(&user_id, password, identity.email)
.await;
.create_local_account(&user_id, Some(password), identity.email)
.await?;
user_id
};
@@ -106,7 +106,7 @@ pub(crate) async fn register_route(
let (token, device) = if !body.inhibit_login {
// If UIAA is disabled, we can't create a device. In that case only appservices
// can reach this point in the first place, so we return an error for them.
if !services.config.oauth.compatibility_mode.uiaa_available() {
if !services.config.oauth.compatibility_mode().uiaa_available() {
return Err!(Request(AppserviceLoginUnsupported(
"User-interactive appservice registration is not available on this server."
)));
+4
View File
@@ -25,6 +25,10 @@ pub(crate) async fn third_party_route(
let sender_user = body.identity.expect_sender_user()?;
let mut threepids = vec![];
if !services.threepid.email_requirement().may_view() {
return Ok(get_3pids::v3::Response::new(vec![]));
}
if let Some(email) = services
.threepid
.get_email_for_localpart(sender_user.localpart())
+13 -16
View File
@@ -12,20 +12,18 @@ pub(crate) async fn get_lock_status(
State(services): State<crate::State>,
body: Ruma<is_user_locked::v1::Request>,
) -> Result<is_user_locked::v1::Response> {
let (admin, active) = join(
let (admin, status) = join(
services.users.is_admin(body.identity.expect_sender_user()?),
services.users.is_active(&body.user_id),
services.users.status(&body.user_id),
)
.await;
if !admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("Can only check the lock status of local users")));
}
if !active {
return Err!(Request(NotFound("Unknown user")));
}
status.ensure_active()?;
Ok(is_user_locked::v1::Response::new(
services.users.is_locked(&body.user_id).await?,
))
@@ -40,9 +38,9 @@ pub(crate) async fn put_lock_status(
) -> Result<lock_user::v1::Response> {
let sender_user = body.identity.expect_sender_user()?;
let (sender_admin, active, target_admin) = join3(
let (sender_admin, status, target_admin) = join3(
services.users.is_admin(sender_user),
services.users.is_active(&body.user_id),
services.users.status(&body.user_id),
services.users.is_admin(&body.user_id),
)
.await;
@@ -50,18 +48,17 @@ pub(crate) async fn put_lock_status(
if !sender_admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("Can only set the lock status of local users")));
}
if !active {
return Err!(Request(NotFound("Unknown user")));
}
status.ensure_active()?;
if body.user_id == *sender_user {
return Err!(Request(Forbidden("You cannot lock yourself")));
}
if target_admin {
return Err!(Request(Forbidden("You cannot lock another server administrator")));
}
if services.users.is_locked(&body.user_id).await? == body.locked {
// No change
return Ok(lock_user::v1::Response::new(body.locked));
+13 -16
View File
@@ -12,20 +12,18 @@ pub(crate) async fn get_suspended_status(
State(services): State<crate::State>,
body: Ruma<is_user_suspended::v1::Request>,
) -> Result<is_user_suspended::v1::Response> {
let (admin, active) = join(
let (admin, status) = join(
services.users.is_admin(body.identity.expect_sender_user()?),
services.users.is_active(&body.user_id),
services.users.status(&body.user_id),
)
.await;
if !admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("Can only check the suspended status of local users")));
}
if !active {
return Err!(Request(NotFound("Unknown user")));
}
status.ensure_active()?;
Ok(is_user_suspended::v1::Response::new(
services.users.is_suspended(&body.user_id).await?,
))
@@ -40,9 +38,9 @@ pub(crate) async fn put_suspended_status(
) -> Result<suspend_user::v1::Response> {
let sender_user = body.identity.expect_sender_user()?;
let (sender_admin, active, target_admin) = join3(
let (sender_admin, status, target_admin) = join3(
services.users.is_admin(sender_user),
services.users.is_active(&body.user_id),
services.users.status(&body.user_id),
services.users.is_admin(&body.user_id),
)
.await;
@@ -50,18 +48,17 @@ pub(crate) async fn put_suspended_status(
if !sender_admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("Can only set the suspended status of local users")));
}
if !active {
return Err!(Request(NotFound("Unknown user")));
}
status.ensure_active()?;
if body.user_id == *sender_user {
return Err!(Request(Forbidden("You cannot suspend yourself")));
}
if target_admin {
return Err!(Request(Forbidden("You cannot suspend another server administrator")));
}
if services.users.is_suspended(&body.user_id).await? == body.suspended {
// No change
return Ok(suspend_user::v1::Response::new(body.suspended));
+7 -2
View File
@@ -7,10 +7,11 @@
api::client::discovery::get_capabilities::{
self,
v3::{
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
ThirdPartyIdChangesCapability,
Capabilities, GetLoginTokenCapability, ProfileFieldsCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
},
},
assign,
};
use crate::Ruma;
@@ -50,5 +51,9 @@ pub(crate) async fn get_capabilities_route(
capabilities.account_moderation.suspend = true;
}
capabilities.profile_fields = Some(
assign!(ProfileFieldsCapability::new(true), { disallowed: Some(services.oidc.restricted_profile_fields()) }),
);
Ok(get_capabilities::v3::Response::new(capabilities))
}
+3 -3
View File
@@ -170,9 +170,7 @@ pub async fn leave_room(
locally."
);
// return the existing leave state, if one exists. `mark_as_left` will then
// update the `roomuserid_leftcount` table, making the leave come down sync
// again.
// return the existing leave state, if one exists
services
.rooms
.state_cache
@@ -207,6 +205,8 @@ pub async fn leave_room(
.update_joined_count(room_id)
.await;
services.sync.wake(user_id).await;
Ok(())
}
+3 -1
View File
@@ -291,7 +291,9 @@ pub(crate) async fn is_ignored_pdu<Pdu>(
{
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
if event.kind().to_string() == "org.matrix.dummy_event" {
if !services.config.send_dummy_events_to_clients
&& event.kind().to_string() == "org.matrix.dummy_event"
{
return Ok(true);
}
+1 -1
View File
@@ -37,7 +37,7 @@ pub(crate) fn router(state: crate::State) -> Router<crate::State> {
.layer(middleware::from_fn_with_state(
state,
async |State(state): State<crate::State>, request: Request, next: Next| -> Response {
if state.config.oauth.compatibility_mode.oauth_available() {
if state.config.oauth.compatibility_mode().oauth_available() {
next.run(request).await
} else {
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
+1 -1
View File
@@ -21,7 +21,7 @@ pub(crate) async fn get_authorization_server_metadata_route(
State(services): State<crate::State>,
_body: Ruma<get_authorization_server_metadata::v1::Request>,
) -> Result<get_authorization_server_metadata::v1::Response> {
if !services.config.oauth.compatibility_mode.oauth_available() {
if !services.config.oauth.compatibility_mode().oauth_available() {
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
}
+58 -289
View File
@@ -1,9 +1,8 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu, utils::to_canonical_object};
use conduwuit::{Err, Result};
use conduwuit_service::Services;
use futures::StreamExt;
use ruma::{
UserId,
api::{
@@ -13,11 +12,10 @@
federation,
},
assign,
events::room::member::MembershipState,
presence::PresenceState,
profile::{ProfileFieldName, ProfileFieldValue},
};
use serde_json::{Value, to_value};
use serde_json::Value;
use service::users::ProfileFieldChange;
use crate::Ruma;
@@ -65,13 +63,24 @@ pub(crate) async fn set_profile_field_route(
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
set_profile_field(
&services,
&body.user_id,
ProfileFieldChange::Set(body.value.clone()),
body.propagate_to.clone(),
)
.await?;
if services
.oidc
.restricted_profile_fields()
.contains(&body.value.field_name())
{
return Err!(Request(Forbidden(
"This profile field is controlled by your identity provider."
)));
}
services
.users
.set_profile_field(
&body.user_id,
ProfileFieldChange::Set(body.value.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(set_profile_field::v3::Response::new())
}
@@ -94,13 +103,24 @@ pub(crate) async fn delete_profile_field_route(
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
set_profile_field(
&services,
&body.user_id,
ProfileFieldChange::Delete(body.field.clone()),
body.propagate_to.clone(),
)
.await?;
if services
.oidc
.restricted_profile_fields()
.contains(&body.field)
{
return Err!(Request(Forbidden(
"This profile field is controlled by your identity provider."
)));
}
services
.users
.set_profile_field(
&body.user_id,
ProfileFieldChange::Delete(body.field.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(delete_profile_field::v3::Response::new())
}
@@ -110,8 +130,8 @@ async fn fetch_full_profile(
user_id: &UserId,
) -> Option<BTreeMap<String, Value>> {
// If the user exists locally, fetch their local profile
if services.users.exists(user_id).await {
return Some(get_local_profile(services, user_id).await);
if services.users.status(user_id).await.is_found() {
return Some(services.users.get_local_profile(user_id).await);
}
// Otherwise ask their homeserver
@@ -135,13 +155,10 @@ async fn fetch_full_profile(
continue;
};
let _ = set_profile_field(
services,
user_id,
ProfileFieldChange::Set(value),
PropagateTo::None,
)
.await;
let _ = services
.users
.set_profile_field(user_id, ProfileFieldChange::Set(value), PropagateTo::None)
.await;
}
Some(BTreeMap::from_iter(response))
@@ -154,7 +171,7 @@ async fn fetch_profile_field(
) -> Result<Option<ProfileFieldValue>> {
// If the user exists locally, fetch their local profile field
if services.globals.user_is_local(user_id) {
return Ok(get_local_profile_field(services, user_id, field).await);
return Ok(services.users.get_local_profile_field(user_id, field).await);
}
// Otherwise ask their homeserver
@@ -175,13 +192,14 @@ async fn fetch_profile_field(
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
let _ = set_profile_field(
services,
user_id,
ProfileFieldChange::Set(value.clone()),
PropagateTo::None,
)
.await;
let _ = services
.users
.set_profile_field(
user_id,
ProfileFieldChange::Set(value.clone()),
PropagateTo::None,
)
.await;
Ok(Some(value))
} else {
@@ -190,260 +208,11 @@ async fn fetch_profile_field(
)))
}
} else {
let _ = set_profile_field(
services,
user_id,
ProfileFieldChange::Delete(field),
PropagateTo::None,
)
.await;
let _ = services
.users
.set_profile_field(user_id, ProfileFieldChange::Delete(field), PropagateTo::None)
.await;
Ok(None)
}
}
pub(crate) async fn get_local_profile(
services: &Services,
user_id: &UserId,
) -> BTreeMap<String, Value> {
let mut profile = BTreeMap::new();
// Get displayname and avatar_url independently because `all_profile_keys`
// doesn't include them
for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] {
let key = field.as_str().to_owned();
if let Some(value) = get_local_profile_field(services, user_id, field).await {
profile.insert(key, value.value().into_owned());
}
}
// Insert all other profile fields
let mut all_fields = services.users.all_profile_keys(user_id);
while let Some((key, value)) = all_fields.next().await {
profile.insert(key, value);
}
profile
}
pub(crate) async fn get_local_profile_field(
services: &Services,
user_id: &UserId,
field: ProfileFieldName,
) -> Option<ProfileFieldValue> {
let value = match field.clone() {
| ProfileFieldName::AvatarUrl => services
.users
.avatar_url(user_id)
.await
.ok()
.map(to_value)
.transpose()
.expect("converting avatar url to value should succeed"),
| ProfileFieldName::DisplayName => services
.users
.displayname(user_id)
.await
.ok()
.map(to_value)
.transpose()
.expect("converting displayname to value should succeed"),
| other => services
.users
.profile_key(user_id, other.as_str())
.await
.ok(),
}?;
Some(
ProfileFieldValue::new(field.as_str(), value)
.expect("local profile field should be valid"),
)
}
enum ProfileFieldChange {
Set(ProfileFieldValue),
Delete(ProfileFieldName),
}
impl ProfileFieldChange {
fn field_name(&self) -> ProfileFieldName {
match self {
| &Self::Delete(ref name) => name.clone(),
| &Self::Set(ref value) => value.field_name(),
}
}
fn value(&self) -> Option<Value> {
if let Self::Set(value) = self {
Some(value.value().into_owned())
} else {
None
}
}
}
async fn set_profile_field(
services: &Services,
user_id: &UserId,
change: ProfileFieldChange,
propagate_to: PropagateTo,
) -> Result<()> {
const MAX_KEY_LENGTH_BYTES: usize = 255;
const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
let field_name = change.field_name();
// TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE,
// M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list
// of error codes and Ruma doesn't have them. Should we return those, or is
// M_TOO_LARGE okay?
if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES {
return Err!(Request(TooLarge(
"Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length."
)));
}
// Serialize the entire profile as canonical JSON, including the new change,
// to check if it exceeds 64 KiB
{
let mut full_profile = get_local_profile(services, user_id).await;
match &change {
| ProfileFieldChange::Set(value) => {
full_profile.insert(
value.field_name().as_str().to_owned(),
value.value().clone().into_owned(),
);
},
| ProfileFieldChange::Delete(key) => {
full_profile.remove(key.as_str());
},
}
if let Ok(canonical_profile) = to_canonical_object(full_profile) {
if serde_json::to_string(&canonical_profile)
.expect("should be able to serialize to string")
.len() > MAX_PROFILE_LENGTH_BYTES
{
return Err!(
"Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in length."
);
}
} else {
return Err!(Request(BadJson("Failed to canonicalize profile.")));
}
}
// If the user is local and changed their displayname or avatar_url, update it
// in all their joined rooms. This is done before updating their profile data
// so we can check the old value of the field if `propagate_to` is `unchanged`.
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
&& matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged)
&& services.globals.user_is_local(user_id)
{
let current_displayname = services.users.displayname(user_id).await.ok();
let current_avatar_url = services.users.avatar_url(user_id).await.ok();
let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);
while let Some(room_id) = all_joined_rooms.next().await {
// TODO: this clobbers any custom fields on the event content
let mut current_membership = services
.rooms
.state_accessor
.get_member(&room_id, user_id)
.await
.expect("should be able to fetch membership event for joined room");
assert_eq!(
current_membership.membership,
MembershipState::Join,
"user should be joined"
);
// If `propagate_to` is `unchanged`, and the current value of the field we're
// updating was changed from its global value in this room, skip it.
if matches!(propagate_to, PropagateTo::Unchanged) {
let field_changed_from_global = match field_name {
| ProfileFieldName::AvatarUrl =>
current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(),
| ProfileFieldName::DisplayName =>
current_membership.displayname.as_ref() != current_displayname.as_ref(),
| _ => unreachable!(),
};
if field_changed_from_global {
continue;
}
}
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
// Preserve keys in accordance with the key copying rules
current_membership.reason = None;
current_membership.join_authorized_via_users_server = None;
match &change {
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
current_membership.avatar_url = Some(avatar_url.clone());
},
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
current_membership.displayname = Some(displayname.clone());
},
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
current_membership.avatar_url = None;
},
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
current_membership.displayname = None;
},
| _ => unreachable!(),
}
let _ = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(user_id.to_string(), &current_membership),
user_id,
Some(&room_id),
&state_lock,
)
.await;
}
if services.config.allow_local_presence {
// Send a presence EDU to indicate the profile changed
let _ = services
.presence
.ping_presence(user_id, &PresenceState::Online)
.await;
}
}
match change {
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
services
.users
.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty()));
},
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
services
.users
.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid()));
},
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
services.users.set_displayname(user_id, None);
},
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
services.users.set_avatar_url(user_id, None);
},
| other =>
services
.users
.set_profile_key(user_id, other.field_name().as_str(), other.value()),
}
Ok(())
}
+4
View File
@@ -107,6 +107,8 @@ pub(crate) async fn set_read_marker_route(
.private_read_set(&body.room_id, sender_user, count);
}
services.sync.wake(sender_user).await;
Ok(set_read_marker::v3::Response::new())
}
@@ -209,5 +211,7 @@ pub(crate) async fn create_receipt_route(
},
}
services.sync.wake(sender_user).await;
Ok(create_receipt::v3::Response::new())
}
+1 -1
View File
@@ -149,7 +149,7 @@ pub(crate) async fn report_user_route(
delay_response().await;
if !services.users.is_active_local(&body.user_id).await {
if !services.users.status(&body.user_id).await.is_found() {
// return 200 as to not reveal if the user exists. Recommended by spec.
return Ok(report_user::v3::Response::new());
}
+10 -3
View File
@@ -337,14 +337,21 @@ pub(crate) async fn create_room_route(
});
let mut power_levels_to_grant = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
if preset == RoomPreset::TrustedPrivateChat {
for recipient_user in &invitees {
power_levels_to_grant.insert(recipient_user.clone(), int!(100));
for recipient_user in invitees.iter().cloned() {
if room_version_rules
.authorization
.explicitly_privilege_room_creators
{
creators.push(recipient_user);
} else {
power_levels_to_grant.insert(recipient_user, int!(100));
}
}
}
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
// Do we care about additional_creators?
if room_version_rules
.authorization
+2 -10
View File
@@ -43,7 +43,7 @@ pub(crate) async fn get_login_types_route(
ClientIp(client): ClientIp,
_body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> {
if !services.config.oauth.compatibility_mode.uiaa_available() {
if !services.config.oauth.compatibility_mode().uiaa_available() {
return Err!(Request(Unrecognized(
"User-interactive authentication is not available on this server."
)));
@@ -88,14 +88,6 @@ pub async fn handle_login(
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
if !services.globals.user_is_local(&user_id) {
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
}
if services.users.is_deactivated(&user_id).await? {
return Err!(Request(UserDeactivated("This account has been deactivated.")));
}
if services.users.is_locked(&user_id).await? {
return Err!(Request(UserLocked("This account has been locked.")));
}
@@ -128,7 +120,7 @@ pub(crate) async fn login_route(
ClientIp(client): ClientIp,
body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> {
if !services.config.oauth.compatibility_mode.uiaa_available() {
if !services.config.oauth.compatibility_mode().uiaa_available() {
return match body.login_info {
| LoginInfo::ApplicationService(_) => {
Err!(Request(AppserviceLoginUnsupported(
+1 -4
View File
@@ -201,9 +201,6 @@ pub(crate) async fn sync_events_route(
.update_device_last_seen(sender_user, Some(sender_device), client_ip)
.await;
// Setup watchers, so if there's no response, we can wait for them
let watcher = services.sync.watch(sender_user, sender_device);
let response = build_sync_events(&services, &body).await?;
if body.body.full_state
|| !(response.rooms.is_empty()
@@ -219,7 +216,7 @@ pub(crate) async fn sync_events_route(
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
let duration = cmp::min(body.body.timeout.unwrap_or(default), default);
_ = tokio::time::timeout(duration, watcher).await;
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
// Retry returning data
build_sync_events(&services, &body).await
+1 -4
View File
@@ -80,9 +80,6 @@ pub(crate) async fn sync_events_v5_route(
let mut body = body.body;
// Setup watchers, so if there's no response, we can wait for them
let watcher = services.sync.watch(sender_user, sender_device);
let next_batch = services.globals.next_count()?;
let conn_id = body.conn_id.clone();
@@ -220,7 +217,7 @@ pub(crate) async fn sync_events_v5_route(
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
let duration = cmp::min(body.timeout.unwrap_or(default), default);
_ = tokio::time::timeout(duration, watcher).await;
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
}
let typing = collect_typing_events(services, sender_user, &body, &todo_rooms).await?;
+30 -26
View File
@@ -32,22 +32,26 @@ pub(crate) async fn search_users_route(
.min(LIMIT_MAX);
let search_term = body.search_term.to_lowercase();
let mut users = services.users.stream().broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok();
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
let mut users = services
.users
.stream_local_users()
.chain(services.users.stream_remote_users())
.broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok();
let display_name_matches = display_name
.as_deref()
.map(str::to_lowercase)
.is_some_and(|display_name| display_name.contains(&search_term));
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
if !user_id_matches && !display_name_matches {
return None;
}
let display_name_matches = display_name
.as_deref()
.map(str::to_lowercase)
.is_some_and(|display_name| display_name.contains(&search_term));
let user_in_public_room =
services
if !user_id_matches && !display_name_matches {
return None;
}
let user_in_public_room = services
.rooms
.state_cache
.rooms_joined(&user_id)
@@ -60,22 +64,22 @@ pub(crate) async fn search_users_route(
.await
});
let user_sees_user = services
.rooms
.state_cache
.user_sees_user(sender_user, &user_id);
let user_sees_user = services
.rooms
.state_cache
.user_sees_user(sender_user, &user_id);
pin_mut!(user_in_public_room, user_sees_user);
pin_mut!(user_in_public_room, user_sees_user);
if user_in_public_room.or(user_sees_user).await {
Some(assign!(search_users::v3::User::new(user_id.clone()), {
display_name,
avatar_url: services.users.avatar_url(&user_id).await.ok(),
}))
} else {
None
}
});
if user_in_public_room.or(user_sees_user).await {
Some(assign!(search_users::v3::User::new(user_id.clone()), {
display_name,
avatar_url: services.users.avatar_url(&user_id).await.ok(),
}))
} else {
None
}
});
let results = users.by_ref().take(limit).collect().await;
let limited = users.next().await.is_some();
+3 -2
View File
@@ -60,8 +60,9 @@ pub(crate) fn sender_device(&self) -> Option<&DeviceId> {
pub(crate) fn expect_sender_device(&self) -> Result<&DeviceId> {
match self {
| Self::User { sender_device, .. } => Ok(sender_device),
| Self::Appservice { .. } =>
| Self::User { sender_device, .. }
| Self::Appservice { sender_device: Some(sender_device), .. } => Ok(sender_device),
| Self::Appservice { sender_device: None, .. } =>
Err!(Request(Forbidden("Appservices must masquerade to use this endpoint."))),
}
}
+3 -7
View File
@@ -4,15 +4,11 @@
use conduwuit::{Err, Event, Result, debug, info, trace, utils::to_canonical_object, warn};
use ruma::{OwnedEventId, api::federation::event::get_missing_events};
use serde_json::{json, value::RawValue};
use service::rooms::event_handler::GET_MISSING_EVENTS_MAX_BATCH_SIZE;
use super::AccessCheck;
use crate::Ruma;
/// arbitrary number but synapse's is 20 and we can handle lots of these anyways
const LIMIT_MAX: usize = 50;
/// spec says default is 10
const LIMIT_DEFAULT: usize = 10;
/// # `POST /_matrix/federation/v1/get_missing_events/{roomId}`
///
/// Retrieves events that the sender is missing.
@@ -45,8 +41,8 @@ pub(crate) async fn get_missing_events_route(
let limit = body
.limit
.try_into()
.unwrap_or(LIMIT_DEFAULT)
.min(LIMIT_MAX);
.unwrap_or(10)
.min(GET_MISSING_EVENTS_MAX_BATCH_SIZE);
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
+2
View File
@@ -203,6 +203,8 @@ pub(crate) async fn create_invite_route(
.update_joined_count(&body.room_id)
.await;
services.sync.wake(&recipient_user).await;
for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&recipient_user) {
let transaction_id = general_purpose::URL_SAFE_NO_PAD
+8 -7
View File
@@ -7,10 +7,7 @@
api::federation::query::{get_profile_information, get_room_information},
};
use crate::{
Ruma,
client::{get_local_profile, get_local_profile_field},
};
use crate::Ruma;
/// # `GET /_matrix/federation/v1/query/directory`
///
@@ -75,15 +72,19 @@ pub(crate) async fn get_profile_information_route(
let response = if let Some(field) = &body.field {
let mut response = get_profile_information::v1::Response::new();
if let Some(value) =
get_local_profile_field(&services, &body.user_id, field.to_owned()).await
if let Some(value) = services
.users
.get_local_profile_field(&body.user_id, field.to_owned())
.await
{
response.set(value.field_name().as_str().to_owned(), value.value().into_owned());
}
response
} else {
get_local_profile(&services, &body.user_id)
services
.users
.get_local_profile(&body.user_id)
.await
.into_iter()
.collect()
+27 -85
View File
@@ -1,5 +1,5 @@
use std::{
collections::{BTreeMap, HashMap, HashSet},
collections::{BTreeMap, HashMap},
net::IpAddr,
time::{Duration, Instant},
};
@@ -7,9 +7,8 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Error, Result, debug, debug_warn, err, error,
Err, Error, Result, debug, debug_error, debug_warn, err, error,
result::LogErr,
state_res::lexicographical_topological_sort,
trace,
utils::{
IterStream, ReadyExt, millis_since_unix_epoch,
@@ -25,8 +24,7 @@
use http::StatusCode;
use itertools::Itertools;
use ruma::{
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
OwnedUserId, RoomId, ServerName, UInt, UserId,
CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
api::{
error::{ErrorKind, LimitExceededErrorData},
federation::transactions::{
@@ -39,12 +37,12 @@
},
},
events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
int,
serde::Raw,
to_device::DeviceIdOrAllDevices,
};
use service::transactions::{
FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse,
use service::{
rooms::event_handler::build_local_dag,
transactions::{FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse},
};
use tokio::sync::watch::{Receiver, Sender};
use tracing::instrument;
@@ -133,6 +131,7 @@ async fn wait_for_result(
}
#[instrument(
name="transaction"
skip_all,
fields(
id = ?body.transaction_id.as_str(),
@@ -174,8 +173,14 @@ async fn process_inbound_transaction(
for (id, result) in &results {
if let Err(e) = result {
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
debug_warn!("Incoming PDU failed {id}: {e:?}");
match e {
| Error::BadRequest(
ErrorKind::Forbidden | ErrorKind::InvalidParam | ErrorKind::BadJson,
..,
) => {
debug_warn!("Incoming PDU {id} failed: {e:?}");
},
| _ => debug_error!("Incoming PDU {id} failed: {e:?}"),
}
}
}
@@ -269,74 +274,6 @@ async fn handle(
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::with_capacity(pdu_map.len());
let mut id_origin_ts: HashMap<OwnedEventId, _> = HashMap::with_capacity(pdu_map.len());
for (event_id, value) in pdu_map {
// We already checked that these properties are correct in parse_incoming_pdu,
// so it's safe to unwrap here.
// We also filter to remove any prev_events that are not in this pdu_map, as we
// need to have at least one event with zero out degrees for the lexico-topo
// sort below. If there are multiple events with omitted prevs, they will be
// ordered by timestamp, then event ID. At that point though, it's unlikely to
// matter.
let prev_events = value
.get("prev_events")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|v| EventId::parse(v.as_str().unwrap()).unwrap())
.filter(|id| pdu_map.contains_key(id))
.collect();
dag.insert(event_id.clone(), prev_events);
let origin_server_ts = value
.get("origin_server_ts")
.and_then(ruma::CanonicalJsonValue::as_integer)
.unwrap_or_default();
id_origin_ts.insert(event_id.clone(), origin_server_ts);
}
debug!(count = dag.len(), "Sorting incoming events with partial graph");
lexicographical_topological_sort(&dag, &async |node_id| {
// 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.
let ts = id_origin_ts
.get(&node_id)
.copied()
.unwrap_or_else(|| int!(0))
.to_string()
.parse::<u64>()
.ok()
.and_then(UInt::new)
.unwrap_or_default();
Ok((int!(0), MilliSecondsSinceUnixEpoch(ts)))
})
.await
.inspect(|sorted| {
debug_assert_eq!(
sorted.len(),
pdu_map.len(),
"Sorted graph was not the same size as the input graph"
);
})
.map_err(|e| err!("failed to resolve local graph: {e}"))
}
async fn handle_room(
services: &Services,
_client: &IpAddr,
@@ -352,7 +289,7 @@ async fn handle_room(
.await;
let room_id = &room_id;
let pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus
let mut pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus
.into_iter()
.map(|(_, event_id, value)| (event_id, value))
.collect();
@@ -360,7 +297,11 @@ async fn handle_room(
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
// responsibility.
let sorted_event_ids = if pdu_map.len() >= 2 {
build_local_dag(&pdu_map).await.unwrap_or_else(|e| {
let refmap = pdu_map
.iter()
.map(|(event_id, obj)| (event_id.clone(), obj))
.collect();
build_local_dag(&refmap).await.unwrap_or_else(|e| {
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
pdu_map.keys().cloned().collect()
})
@@ -370,9 +311,8 @@ async fn handle_room(
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();
.remove(&event_id)
.expect("sorted event IDs must be from the original map");
services
.server
.check_running()
@@ -380,7 +320,8 @@ async fn handle_room(
let result = services
.rooms
.event_handler
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
.handle_incoming_pdu(origin, room_id, &event_id, value.clone(), true)
.boxed()
.await
.map(|_| ());
results.push((event_id, result));
@@ -679,8 +620,9 @@ async fn handle_edu_direct_to_device(
.broad_filter_map(|(target_user_id, map)| async move {
services
.users
.is_active_local(&target_user_id)
.status(&target_user_id)
.await
.is_active()
.then_some((target_user_id, map))
})
.for_each_concurrent(automatic_width(), |(target_user_id, map)| {
+3
View File
@@ -9,6 +9,7 @@ repository.workspace = true
version.workspace = true
[lib]
doctest = false
path = "mod.rs"
crate-type = [
"rlib",
@@ -117,6 +118,8 @@ url.workspace = true
parking_lot.workspace = true
lock_api.workspace = true
hyper-util.workspace = true
resolvematrix.workspace = true
openidconnect.workspace = true
[target.'cfg(unix)'.dependencies]
nix.workspace = true
+9
View File
@@ -289,6 +289,15 @@ pub fn check(config: &Config) -> Result {
| _ => (),
}
if let Some(oidc) = &config.oauth.oidc
&& oidc.client_secret.is_none()
&& oidc.client_secret_file.is_none()
{
return Err!(
"Either `client_secret` or `client_secret_file` must be set if OIDC is configured."
);
}
Ok(())
}
+159 -5
View File
@@ -4,7 +4,7 @@
pub mod proxy;
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
};
@@ -17,10 +17,12 @@
use figment::providers::{Env, Format, Toml};
pub use figment::{Figment, value::Value as FigmentValue};
use lettre::message::Mailbox;
use openidconnect::{ClientId, ClientSecret, Scope};
use regex::RegexSet;
use ruma::{
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::{discovery::discover_support::ContactRole, rtc::RtcTransport},
profile::ProfileFieldName,
serde::Base64,
};
use serde::{Deserialize, Serialize, de::IgnoredAny};
@@ -375,7 +377,7 @@ pub struct Config {
#[serde(default = "default_max_request_size")]
pub max_request_size: usize,
/// default: 192
/// default: 1024
#[serde(default = "default_max_fetch_prev_events")]
pub max_fetch_prev_events: u16,
@@ -787,6 +789,16 @@ pub struct Config {
/// a substitute for moderation bots.
pub default_room_acl_deny: Option<Vec<String>>,
/// The number of forward extremities to tolerate in a room before
/// attempting to manually squash them with a "dummy event". Setting this
/// above 20 will hinder its efficacy, and setting it below 5 will cause
/// more dummy events to be sent than necessary (which increases federation
/// traffic).
///
/// default: 10
#[serde(default = "default_extremity_threshold")]
pub dummy_event_threshold: u8,
/// display: nested
#[serde(default)]
pub well_known: WellKnownConfig,
@@ -1657,6 +1669,11 @@ pub struct Config {
#[serde(default)]
pub send_messages_from_ignored_users_to_client: bool,
/// Send "org.matrix.dummy_event" events to the client. This is a debugging
/// option.
#[serde(default)]
pub send_dummy_events_to_clients: bool,
/// Vector list of IPv4 and IPv6 CIDR ranges / subnets *in quotes* that you
/// do not want continuwuity to send outbound requests to. Defaults to
/// RFC1918, unroutable, loopback, multicast, and testnet addresses for
@@ -2404,10 +2421,24 @@ pub struct OauthConfig {
/// legacy authentication will be unable to log in.
///
/// default: "hybrid"
pub compatibility_mode: OAuthMode,
compatibility_mode: OAuthMode,
/// display: hidden
pub oidc: Option<OidcConfig>,
}
#[derive(Clone, Debug, Default, Deserialize)]
impl OauthConfig {
#[must_use]
pub fn compatibility_mode(&self) -> OAuthMode {
if self.oidc.is_some() {
OAuthMode::Exclusive
} else {
self.compatibility_mode
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OAuthMode {
Disabled,
@@ -2424,6 +2455,121 @@ pub fn uiaa_available(&self) -> bool { matches!(self, Self::Disabled | Self::Hyb
pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) }
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.oauth.oidc",
optional = "true",
subheader = "\
# Uncommenting this section will enable Continuwuity's support for
# authenticating users using an OpenID Connect-compatible identity provider.
# This is referred to as \"delegated authentication\".
#
# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if
# the `global.oauth.compatibility_mode` setting is set to `exclusive`.
# Matrix clients which do not support OAuth login (also referred to as \"next-gen auth\") will \
NOT be able
# to log in while delegated authentication is active."
)]
pub struct OidcConfig {
/// The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to
/// automatically fetch the identity provider's metadata from this URL.
/// Generally you should set this to the base domain your identity provider
/// runs on.
pub discovery_url: Url,
/// The OAuth client ID for Continuwuity to use when communicating with the
/// identity provider.
pub client_id: ClientId,
/// The OAuth client secret for Continuwuity to use when communicating with
/// the identity provider.
pub client_secret: Option<ClientSecret>,
/// A path to a file which Continuwuity will read the client secret from.
/// If this option is set, it will override `client_secret`.
///
/// The server will fail to start if the file cannot be read.
pub client_secret_file: Option<PathBuf>,
/// Additional scopes Continuwuity should request from the IDP. This may be
/// necessary to access certain claims. Continuwuity always requests the
/// `openid` scope.
///
/// default: []
#[serde(default)]
pub additional_scopes: Vec<Scope>,
/// Whether the user should be prompted to choose a localpart
/// when signing in for the first time. If this is `false`, Continuwuity
/// will attempt to use the value of the `preferred_username_claim`
/// (see below) as the user's localpart. Authentication will
/// fail if this claim is missing or is not a valid localpart.
///
/// default: true
#[serde(default = "true_fn")]
pub prompt_for_localpart: bool,
/// The claim to use for the user's localpart, if `prompt_for_localpart` is
/// false.
///
/// default: "preferred_username"
#[serde(default = "default_preferred_username_claim")]
pub preferred_username_claim: String,
/// The claim which will be used to set the user's email address,
/// either on initial registration or on every login depending on
/// the value of `profile_key_import_mode`. Continuwuity assumes that
/// the IDP has taken care of verifying that the user controls the email
/// address it provides.
///
/// This option does nothing if SMTP is not configured.
///
/// If this option is set, and `profile_key_import_mode` is `on_login`,
/// users will not be able to change their email addresses themselves.
///
/// default: "email"
pub email_claim: Option<String>,
/// Defines how claims returned from the IDP should be mapped to a user's
/// profile data. The profile field named in each key will be set from the
/// claim named in the corresponding value when the user first registers,
/// and possibly on subsequent logins as well, depending on the value of
/// `profile_key_import_mode` (see below).
///
/// Per-room overrides to the user's display name or avatar will be
/// preserved by the import process.
///
/// SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will
/// perform a HTTP GET to the URL in the mapped claim and use the returned
/// file as the user's profile picture. Make sure your users are not able
/// to set the value of the mapped claim to an arbitrary URL.
///
/// default: { displayname = "name" }
#[serde(default = "default_profile_key_map")]
pub profile_key_map: HashMap<String, String>,
/// When profile keys should be imported from the IDP's claims.
///
/// - "on_registration": Listed keys will be imported once, when the user
/// logs in for the first time and their shadow account is created.
/// - "on_login": Listed keys will be imported every time the user logs in.
/// Additionally, users will not be able to manually edit any listed keys
/// through their Matrix client.
///
/// default: "on_registration"
#[serde(default)]
pub profile_key_import_mode: OidcProfileKeyImportMode,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OidcProfileKeyImportMode {
#[default]
OnRegistration,
OnLogin,
}
const DEPRECATED_KEYS: &[&str] = &[
"cache_capacity",
"conduit_cache_capacity_modifier",
@@ -2615,7 +2761,7 @@ fn default_pusher_timeout() -> u64 { 60 }
fn default_pusher_idle_timeout() -> u64 { 15 }
fn default_max_fetch_prev_events() -> u16 { 192_u16 }
fn default_max_fetch_prev_events() -> u16 { 1024 }
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
@@ -2718,6 +2864,8 @@ fn default_rocksdb_stats_level() -> u8 { 1 }
#[inline]
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V12 }
fn default_extremity_threshold() -> u8 { 10 }
fn default_ip_range_denylist() -> Vec<String> {
vec![
"127.0.0.0/8".to_owned(),
@@ -2806,3 +2954,9 @@ fn default_client_shutdown_timeout() -> u64 { 15 }
fn default_sender_shutdown_timeout() -> u64 { 5 }
fn default_terms_language() -> String { "en".to_owned() }
fn default_preferred_username_claim() -> String { "preferred_username".to_owned() }
fn default_profile_key_map() -> HashMap<String, String> {
HashMap::from_iter([(ProfileFieldName::DisplayName.to_string(), "name".to_owned())])
}
+2
View File
@@ -87,6 +87,8 @@ pub enum Error {
YamlDe(#[from] serde_saphyr::Error),
#[error(transparent)]
YamlSer(#[from] serde_saphyr::ser_error::Error),
#[error(transparent)]
ResolveServer(#[from] resolvematrix::error::ResolveServerError),
// ruma/conduwuit
#[error("Arithmetic operation failed: {0}")]
+5 -2
View File
@@ -5,7 +5,10 @@
events::{MessageLikeEventContent, StateEventContent, TimelineEventType},
};
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value},
};
use super::StateKey;
@@ -62,7 +65,7 @@ impl Default for PartialPdu {
fn default() -> Self {
Self {
event_type: "m.room.message".into(),
content: Box::<RawJsonValue>::default(),
content: to_raw_value(&json!({})).unwrap(),
unsigned: None,
state_key: None,
redacts: None,
+3
View File
@@ -1,7 +1,10 @@
pub mod exponential_backoff;
pub mod jitter;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub use jitter::jitter;
use crate::{Result, err};
#[inline]
+16
View File
@@ -0,0 +1,16 @@
use std::{ops::RangeInclusive, time::Duration};
/// Returns a `Duration` that is jittered by a random percentage of the base
/// duration. The jitter is applied as a random percentage in the range of
/// `-jitter_range` to `jitter_range`.
///
/// # Example
/// ```
/// use conduwuit_core::utils::time::jitter;
/// let sleep_duration = jitter(Duration::from_secs(1), -10..=10);
/// // Adds a jitter of between -10% and 10% to the duration.
/// ```
#[must_use]
pub fn jitter(base: Duration, jitter_range: RangeInclusive<f64>) -> Duration {
base.mul_f64(1.0 + (rand::random_range(jitter_range)) / 100.0)
}
+20
View File
@@ -124,6 +124,14 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "onetimekeyid_onetimekeys",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "openidsubject_localpart",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "openidsubject_currentpictureurl",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "fallbackkeyid_fallbackkey",
..descriptor::RANDOM_SMALL
@@ -169,6 +177,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "registrationtoken_info",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "remoteuserid_remoteuser",
..descriptor::RANDOM
},
Descriptor {
name: "roomid_invitedcount",
..descriptor::RANDOM_SMALL
@@ -195,6 +207,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
val_size_hint: Some(8),
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomid_mindepth",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomserverids",
..descriptor::RANDOM_SMALL
@@ -394,6 +410,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "userid_blurhash",
..descriptor::DROPPED
},
Descriptor {
name: "userid_deactivated",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_dehydrateddevice",
..descriptor::RANDOM_SMALL
+6
View File
@@ -81,6 +81,12 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
};
file.write_fmt(format_args!("{section_header}"))
.expect("written to config file");
if let Some(subheader) = settings.get("subheader") {
file.write_all(subheader.as_bytes())
.expect("written to config file");
file.write_all(b"\n\n").expect("written to config file");
}
}
let mut summary: Vec<TokenStream2> = Vec::new();
+2
View File
@@ -119,7 +119,9 @@ recaptcha-verify = { version = "0.2.0", default-features = false }
reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated
yansi.workspace = true
lettre.workspace = true
resolvematrix.workspace = true
serde_urlencoded.workspace = true
openidconnect.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true
+5 -1
View File
@@ -16,7 +16,7 @@
};
use serde::Deserialize;
use crate::{Dep, globals};
use crate::{Dep, globals, sync};
#[derive(Debug)]
pub enum AnyRawAccountDataEvent {
@@ -36,6 +36,7 @@ struct Data {
struct Services {
globals: Dep<globals::Service>,
sync: Dep<sync::Service>,
}
impl crate::Service for Service {
@@ -43,6 +44,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
globals: args.depend::<globals::Service>("globals"),
sync: args.depend::<sync::Service>("sync"),
},
db: Data {
roomuserdataid_accountdata: args.db["roomuserdataid_accountdata"].clone(),
@@ -84,6 +86,8 @@ pub async fn update(
self.db.roomuserdataid_accountdata.remove(&prev);
}
self.services.sync.wake(user_id).await;
Ok(())
}
+1 -1
View File
@@ -48,7 +48,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
// Create a user for the server
let server_user = services.globals.server_user.as_ref();
services.users.create(server_user, None)?;
services.users.create_shadow_account(server_user).await?;
let mut create_content = if room_version_rules.authorization.use_room_create_sender {
RoomCreateEventContent::new_v1(server_user.into())
+6 -5
View File
@@ -18,7 +18,11 @@
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{Result, Server, debug, error, utils::response::LimitReadExt, warn};
use conduwuit::{
Result, Server, debug, error,
utils::{response::LimitReadExt, time::jitter},
warn,
};
use database::{Deserialized, Map};
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
use serde::Deserialize;
@@ -98,10 +102,7 @@ async fn worker(self: Arc<Self>) -> Result<()> {
.ok();
}
let first_check_jitter = {
let jitter_percent = rand::random_range(-50.0..=10.0);
self.interval.mul_f64(1.0 + jitter_percent / 100.0)
};
let first_check_jitter = jitter(self.interval, -50.0..=10.0);
let mut i = interval(self.interval);
i.set_missed_tick_behavior(MissedTickBehavior::Delay);
+7 -7
View File
@@ -108,17 +108,17 @@ async fn start_appservice(&self, id: String, registration: Registration) -> Resu
self.services.globals.server_name(),
)?;
if !self.services.users.exists(&appservice_user_id).await {
self.services.users.create(&appservice_user_id, None)?;
} else if self
if !self
.services
.users
.is_deactivated(&appservice_user_id)
.status(&appservice_user_id)
.await
.unwrap_or(false)
.is_found()
{
// Reactivate the appservice user if it was accidentally deactivated
self.services.users.set_password(&appservice_user_id, None);
self.services
.users
.create_shadow_account(&appservice_user_id)
.await?;
}
self.registration_info
+9 -19
View File
@@ -11,7 +11,6 @@ pub struct Service {
pub default: reqwest::Client,
pub url_preview: reqwest::Client,
pub extern_media: reqwest::Client,
pub well_known: reqwest::Client,
pub federation: reqwest::Client,
pub federation_slow: reqwest::Client,
pub sender: reqwest::Client,
@@ -43,7 +42,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
default: base(config)?
.dns_resolver(resolver.resolver.clone())
.dns_resolver(resolver.dns.resolver.clone())
.build()?,
url_preview: base(config)
@@ -51,28 +50,19 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
builder_interface(builder, url_preview_bind_iface.as_deref())
})?
.local_address(url_preview_bind_addr)
.dns_resolver(resolver.resolver.clone())
.dns_resolver(resolver.dns.resolver.clone())
.timeout(Duration::from_secs(config.url_preview_timeout))
.redirect(redirect::Policy::limited(3))
.user_agent(url_preview_user_agent)
.build()?,
extern_media: base(config)?
.dns_resolver(resolver.resolver.clone())
.dns_resolver(resolver.dns.resolver.clone())
.redirect(redirect::Policy::limited(3))
.build()?,
well_known: base(config)?
.dns_resolver(resolver.resolver.clone())
.connect_timeout(Duration::from_secs(config.well_known_conn_timeout))
.read_timeout(Duration::from_secs(config.well_known_timeout))
.timeout(Duration::from_secs(config.well_known_timeout))
.pool_max_idle_per_host(0)
.redirect(redirect::Policy::limited(4))
.build()?,
federation: base(config)?
.dns_resolver(resolver.resolver.hooked.clone())
.dns_resolver(resolver.dns.resolver.hooked.clone())
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
.read_timeout(Duration::from_secs(config.federation_timeout))
.timeout(Duration::from_secs(
@@ -86,7 +76,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
.build()?,
federation_slow: base(config)?
.dns_resolver(resolver.resolver.hooked.clone())
.dns_resolver(resolver.dns.resolver.hooked.clone())
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
.read_timeout(Duration::from_secs(config.federation_timeout.saturating_mul(6)))
.timeout(Duration::from_secs(
@@ -100,7 +90,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
.build()?,
sender: base(config)?
.dns_resolver(resolver.resolver.hooked.clone())
.dns_resolver(resolver.dns.resolver.hooked.clone())
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
.read_timeout(Duration::from_secs(config.sender_timeout))
.timeout(Duration::from_secs(config.sender_timeout))
@@ -110,7 +100,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
.build()?,
appservice: base(config)?
.dns_resolver(resolver.resolver.clone())
.dns_resolver(resolver.dns.resolver.clone())
.connect_timeout(Duration::from_secs(5))
.read_timeout(Duration::from_secs(config.appservice_timeout))
.timeout(Duration::from_secs(config.appservice_timeout))
@@ -120,7 +110,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
.build()?,
pusher: base(config)?
.dns_resolver(resolver.resolver.clone())
.dns_resolver(resolver.dns.resolver.clone())
.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
.timeout(Duration::from_secs(config.pusher_timeout))
.pool_max_idle_per_host(1)
@@ -151,7 +141,7 @@ pub fn valid_cidr_range(&self, ip: &IPAddress) -> bool {
}
}
fn base(config: &Config) -> Result<reqwest::ClientBuilder> {
pub fn base(config: &Config) -> Result<reqwest::ClientBuilder> {
let mut builder = reqwest::Client::builder()
.hickory_dns(true)
.connect_timeout(Duration::from_secs(config.request_conn_timeout))
+16 -9
View File
@@ -54,15 +54,22 @@ impl Service {
async fn set_emergency_access(&self) -> Result {
let server_user = &self.services.globals.server_user;
self.services.users.set_password(
server_user,
self.services
.config
.emergency_password
.as_deref()
.map(HashedPassword::new)
.transpose()?,
);
match &self.services.config.emergency_password {
| Some(emergency_password) => {
let emergency_password = HashedPassword::new(emergency_password)?;
self.services
.users
.convert_to_local_account(server_user, emergency_password)
.await?;
},
| None => {
self.services
.users
.convert_to_shadow_account(server_user)
.await?;
},
}
let (ruleset, pwd_set) = match self.services.config.emergency_password {
| Some(_) => (Ruleset::server_default(server_user), true),
+17 -9
View File
@@ -7,6 +7,7 @@
};
use ipaddress::IPAddress;
use reqwest::{Client, Method, Request, Response, Url};
use resolvematrix::resolution::Resolution;
use ruma::{
ServerName,
api::{
@@ -18,7 +19,7 @@
},
};
use crate::{SUPPORTED_VERSIONS, resolver::actual::ActualDest};
use crate::SUPPORTED_VERSIONS;
impl super::Service {
/// Sends a signed request to a remote server over federation.
@@ -130,10 +131,15 @@ pub async fn execute_on<'i, T, PathBuilderInput>(
))));
}
let actual = self.services.resolver.get_actual_dest(dest).await?;
let actual = self
.services
.resolver
.resolver
.resolve_server(dest.as_str())
.await?;
let request = Request::try_from(request.try_into_http_request::<Vec<u8>>(
actual.string().as_str(),
actual.base_url().as_str(),
authentication,
PathBuilderInput::create(),
)?)?;
@@ -150,7 +156,7 @@ pub async fn execute_on<'i, T, PathBuilderInput>(
async fn perform<T>(
&self,
dest: &ServerName,
actual: &ActualDest,
actual: &Resolution,
request: Request,
client: &Client,
) -> Result<T::IncomingResponse>
@@ -186,7 +192,9 @@ fn validate_url(&self, url: &Url) -> Result<()> {
if let Some(url_host) = url.host_str() {
if let Ok(ip) = IPAddress::parse(url_host) {
trace!("Checking request URL IP {ip:?}");
self.services.resolver.validate_ip(&ip)?;
if !self.services.client.valid_cidr_range(&ip) {
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
}
}
}
@@ -198,7 +206,7 @@ fn validate_url(&self, url: &Url) -> Result<()> {
async fn handle_response<T>(
&self,
dest: &ServerName,
actual: &ActualDest,
actual: &Resolution,
method: &Method,
url: &Url,
response: Response,
@@ -231,7 +239,7 @@ async fn handle_response<T>(
async fn into_http_response(
dest: &ServerName,
actual: &ActualDest,
actual: &Resolution,
method: &Method,
url: &Url,
mut response: Response,
@@ -243,7 +251,7 @@ async fn into_http_response(
request_url = %url,
response_url = %response.url(),
"Received response from {}",
actual.string(),
actual.base_url(),
);
let mut http_response_builder = http::Response::builder()
@@ -280,7 +288,7 @@ async fn into_http_response(
}
fn handle_error(
actual: &ActualDest,
actual: &Resolution,
method: &Method,
url: &Url,
mut e: reqwest::Error,
-6
View File
@@ -211,7 +211,6 @@ pub async fn download_audio(
Ok(preview_data)
}
#[cfg(feature = "url_preview")]
pub async fn download_media(&self, url: &str) -> Result<(OwnedMxcUri, usize)> {
use conduwuit::utils::random_string;
use http::header::CONTENT_TYPE;
@@ -268,11 +267,6 @@ pub async fn download_audio(
Err!(FeatureDisabled("url_preview"))
}
#[cfg(not(feature = "url_preview"))]
pub async fn download_media(&self, _url: &str) -> Result<UrlPreviewData> {
Err!(FeatureDisabled("url_preview"))
}
#[cfg(feature = "url_preview")]
async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
use webpage::HTML;
+57 -4
View File
@@ -41,7 +41,7 @@ pub(crate) async fn migrations(services: &Services) -> Result<()> {
// requires recreating the database from scratch.
if users_count > 0 {
let server_user = &services.globals.server_user;
if !services.users.exists(server_user).await {
if !services.users.status(server_user).await.is_found() {
error!("The {server_user} server user does not exist, and the database is not new.");
return Err!(Database(
"Cannot reuse an existing database after changing the server name, please \
@@ -228,6 +228,18 @@ async fn migrate(services: &Services) -> Result<()> {
.map_err(|e| err!("Failed to run 'fix_local_invite_state' migration': {e}"))?;
}
if services.globals.db.database_version().await < 18 {
services.globals.db.bump_database_version(18);
info!("Migration: Bumped database version to 18");
}
if db["global"].get(SPLIT_USERID_PASSWORD).await.is_not_found() {
info!("Running migration 'split_userid_password'");
split_userid_password(services)
.await
.map_err(|e| err!("Failed to run 'split_userid_password' migration': {e}"))?;
}
assert_eq!(
services.globals.db.database_version().await,
DATABASE_VERSION,
@@ -242,9 +254,9 @@ async fn migrate(services: &Services) -> Result<()> {
if !patterns.is_empty() {
services
.users
.stream()
.stream_local_users()
.filter_map(async |user_id| {
if services.users.is_active_local(&user_id).await {
if services.users.status(&user_id).await.is_found() {
Some(user_id)
} else {
None
@@ -774,7 +786,7 @@ async fn fix_local_invite_state(services: &Services) -> Result {
let db = &services.db;
let cork = db.cork_and_sync();
let userroomid_invitestate = services.db["userroomid_invitestate"].clone();
let userroomid_invitestate = db["userroomid_invitestate"].clone();
// for each user invited to a room
let fixed = userroomid_invitestate.stream()
@@ -818,3 +830,44 @@ async fn fix_local_invite_state(services: &Services) -> Result {
db.db.sort()?;
Ok(())
}
const SPLIT_USERID_PASSWORD: &str = "split_userid_password";
async fn split_userid_password(services: &Services) -> Result {
// Split remote and deactivated users out from the `userid_password` table
let db = &services.db;
let cork = db.cork_and_sync();
let userid_password = db["userid_password"].clone();
let remoteuserid_remoteuser = db["remoteuserid_remoteuser"].clone();
let userid_deactivated = db["userid_deactivated"].clone();
let remote_users = userid_password
.stream::<OwnedUserId, String>()
.ignore_err()
.fold(0_usize, async |mut remote_users, (user_id, hash)| {
if !services.globals.user_is_local(&user_id) {
assert!(hash.is_empty(), "non-empty hash {hash} for remote user {user_id}");
remoteuserid_remoteuser.insert(&user_id, "");
userid_password.remove(&user_id);
remote_users = remote_users.saturating_add(1);
} else if hash.is_empty() {
if !(services.appservice.is_exclusive_user_id(&user_id).await
|| user_id == services.globals.server_user)
{
info!("Marking {user_id} as deactivated");
userid_deactivated.insert(&user_id, "");
}
}
remote_users
})
.await;
drop(cork);
info!(?remote_users, "Split userid_password.");
db["global"].insert(FIXED_LOCAL_INVITE_STATE_MARKER, []);
db.db.sort()?;
Ok(())
}
+1
View File
@@ -28,6 +28,7 @@
pub mod media;
pub mod moderation;
pub mod oauth;
pub mod oidc;
pub mod presence;
pub mod pusher;
pub mod registration_tokens;
+1 -1
View File
@@ -160,7 +160,7 @@ pub enum ErrorCode {
InvalidClientMetadata,
}
#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct AuthorizationCodeResponse {
pub state: String,
pub code: String,
+484
View File
@@ -0,0 +1,484 @@
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{
Result,
config::{OidcConfig, OidcProfileKeyImportMode},
debug, err, error, info, warn,
};
use database::{Deserialized, Map};
use lettre::Address;
use openidconnect::{
AdditionalClaims, AuthorizationCode, ClientSecret, CsrfToken, EmptyExtraTokenFields,
EndpointMaybeSet, EndpointNotSet, EndpointSet, IdTokenClaims, IdTokenFields, IssuerUrl,
Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, StandardErrorResponse,
StandardTokenResponse, TokenResponse,
core::{
CoreAuthDisplay, CoreAuthPrompt, CoreAuthenticationFlow, CoreErrorResponseType,
CoreGenderClaim, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreRevocableToken,
CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType,
},
reqwest,
};
use ruma::{
OwnedUserId, UserId,
api::client::profile::PropagateTo,
profile::{ProfileFieldName, ProfileFieldValue},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::{runtime, sync::SetOnce};
use url::Url;
use crate::{
Dep, config, globals, media,
oauth::grant::AuthorizationCodeResponse,
threepid,
users::{self, AccountStatus, ProfileFieldChange},
};
pub struct Service {
services: Services,
runtime: runtime::Handle,
db: Data,
client: Option<OidcClient>,
}
struct Data {
openidsubject_localpart: Arc<Map>,
openidsubject_currentpictureurl: Arc<Map>,
}
struct Services {
config: Dep<config::Service>,
globals: Dep<globals::Service>,
media: Dep<media::Service>,
threepid: Dep<threepid::Service>,
users: Dep<users::Service>,
}
struct OidcClient {
config: OidcConfig,
client_secret: ClientSecret,
machine: SetOnce<OidcClientMachine>,
client: reqwest::Client,
}
type OidcClientMachine = openidconnect::Client<
AllClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
StandardTokenResponse<
IdTokenFields<
AllClaims,
EmptyExtraTokenFields,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
>,
CoreTokenType,
>,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointMaybeSet,
EndpointMaybeSet,
>;
pub type Claims = IdTokenClaims<AllClaims, CoreGenderClaim>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AllClaims {
#[serde(flatten)]
pub claims: HashMap<String, Value>,
}
impl AdditionalClaims for AllClaims {}
#[derive(Debug, Deserialize, Serialize)]
pub struct PendingSession {
pkce_verifier: PkceCodeVerifier,
nonce: Nonce,
csrf_token: CsrfToken,
}
pub enum SessionCompletionStatus {
NeedsUserId,
Complete(OwnedUserId),
}
#[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"),
globals: args.depend::<globals::Service>("globals"),
media: args.depend::<media::Service>("media"),
threepid: args.depend::<threepid::Service>("threepid"),
users: args.depend::<users::Service>("users"),
},
runtime: args.server.runtime().clone(),
db: Data {
openidsubject_localpart: args.db["openidsubject_localpart"].clone(),
openidsubject_currentpictureurl: args.db["openidsubject_currentpictureurl"].clone(),
},
client: args.server.config.oauth.oidc.as_ref().map(|config| -> Result<OidcClient> {
Ok(OidcClient {
config: config.clone(),
client_secret: if let Some(client_secret_file) = &config.client_secret_file {
std::fs::read_to_string(client_secret_file)
.map(ClientSecret::new)
.map_err(|err| err!("Failed to read OIDC client secret file: {err}"))?
} else if let Some(client_secret) = &config.client_secret {
client_secret.clone()
} else {
// The config check function should cause an early exit before this happens
panic!("neither client secret or client secret file were set");
},
machine: SetOnce::new(),
// This isn't in the client service because it has to use the `reqwest` shipped by `openidconnect`
client: reqwest::ClientBuilder::new()
.connect_timeout(Duration::from_secs(args.server.config.request_conn_timeout))
.read_timeout(Duration::from_secs(args.server.config.request_timeout))
.timeout(Duration::from_secs(args.server.config.request_total_timeout))
.pool_idle_timeout(Duration::from_secs(args.server.config.request_idle_timeout))
.pool_max_idle_per_host(args.server.config.request_idle_per_host.into())
.user_agent(conduwuit::user_agent())
.redirect(reqwest::redirect::Policy::none())
.danger_accept_invalid_certs(args.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
.build()
.expect("client should build")
})}
).transpose()?,
}))
}
async fn worker(self: Arc<Self>) -> Result {
if let Some(OidcClient { config, client_secret, machine, client }) = &self.client {
let redirect_url = self
.services
.config
.get_client_domain()
.join(&format!("{}/oidc/complete", conduwuit::ROUTE_PREFIX))
.expect("redirect url should be valid");
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::from_url(config.discovery_url.clone()),
client,
)
.await
.map_err(|err| err!("Failed to discover OIDC provider metadata: {err}"))?;
machine
.set(
OidcClientMachine::from_provider_metadata(
provider_metadata,
config.client_id.clone(),
Some(client_secret.clone()),
)
.set_redirect_uri(RedirectUrl::from_url(redirect_url)),
)
.expect("machine should be empty");
}
Ok(())
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
const SERVER_MISCONFIGURED: &str =
"Identity server is misconfigured. Contact your homeserver's administrator.";
pub fn enabled(&self) -> bool { self.client.is_some() }
pub fn restricted_profile_fields(&self) -> Vec<ProfileFieldName> {
if let Some(config) = self.client.as_ref().map(|client| &client.config)
&& matches!(config.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin)
{
config
.profile_key_map
.keys()
.map(|key| ProfileFieldName::from(key.as_str()))
.collect()
} else {
vec![]
}
}
pub async fn begin_session(&self, prompt: Option<CoreAuthPrompt>) -> (PendingSession, Url) {
let OidcClient { machine, config, .. } =
self.client.as_ref().expect("oidc should be configured");
let machine = machine.wait().await;
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let mut auth_url = machine
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.add_scopes(config.additional_scopes.iter().cloned())
.set_pkce_challenge(pkce_challenge);
if let Some(prompt) = prompt {
auth_url = auth_url.add_prompt(prompt);
}
let (auth_url, csrf_token, nonce) = auth_url.url();
(PendingSession { pkce_verifier, nonce, csrf_token }, auth_url)
}
pub async fn exchange_code(
&self,
session: PendingSession,
response: AuthorizationCodeResponse,
) -> Result<Claims, &'static str> {
let Some(OidcClient { machine, client, .. }) = self.client.as_ref() else {
return Err("Delegated authentication is not enabled on this server.");
};
let machine = machine.wait().await;
if session.csrf_token.into_secret() != response.state {
return Err("State mismatch.");
}
let token_response = machine
.exchange_code(AuthorizationCode::new(response.code))
.expect("machine should be configured correctly")
.set_pkce_verifier(session.pkce_verifier)
.request_async(client)
.await
.map_err(|err| {
error!("Failed to exchange OIDC authorization code: {err}");
"Code exchange failed."
})?;
let Some(id_token) = token_response.id_token() else {
error!("Identity server did not return an id token");
return Err(Self::SERVER_MISCONFIGURED);
};
let claims = id_token
.claims(&machine.id_token_verifier(), &session.nonce)
.map_err(|err| {
error!("Failed to verify id token claims: {err}");
Self::SERVER_MISCONFIGURED
})?
.to_owned();
info!(subject = claims.subject().as_str(), "Authenticated subject");
Ok(claims)
}
#[tracing::instrument(skip(self, claims), fields(subject = claims.subject().to_string()))]
pub async fn complete_session(
&self,
claims: &Claims,
supplied_user_id: Option<OwnedUserId>,
) -> Result<SessionCompletionStatus, &'static str> {
let Some(OidcClient { config, .. }) = self.client.as_ref() else {
return Err("Delegated authentication is not enabled on this server.");
};
// this is a truly awful hack but we really need all the claims in a map
let all_claims = serde_json::to_value(claims)
.expect("should be able to serialize claims")
.as_object()
.expect("claims should be an object")
.to_owned();
debug!(?all_claims);
let subject = claims.subject().as_str();
let user_id = if let Ok(localpart) = self
.db
.openidsubject_localpart
.get(subject)
.await
.deserialized::<String>()
{
UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name()))
.expect("saved localpart should be valid")
} else if config.prompt_for_localpart {
if let Some(supplied_user_id) = supplied_user_id {
supplied_user_id
} else {
return Ok(SessionCompletionStatus::NeedsUserId);
}
} else if let Some(preferred_username) = all_claims
.get(&config.preferred_username_claim)
.and_then(|claim| claim.as_str())
{
self.services
.users
.determine_registration_user_id(Some(preferred_username.to_owned()), None, None)
.await
.map_err(|err| {
error!("Preferred username claim is not a valid localpart: {err}");
"Your preferred username is not a valid Matrix user ID localpart. Contact \
your homeserver's administrator."
})?
} else {
error!("Preferred username claim was not present or was not a string");
return Err(Self::SERVER_MISCONFIGURED);
};
info!(?subject, ?user_id, "User {user_id} successfully authorized with OIDC");
// Create a shadow account for the user if necessary
let new_account_registered = match self.services.users.status(&user_id).await {
| AccountStatus::Active => {
// Do nothing, an account already exists
false
},
| AccountStatus::NotFound => {
// Create a new shadow user
self.services
.users
.create_local_account(&user_id, None, None)
.await
.map_err(|err| {
error!("Failed to create a shadow user for {user_id}: {err}");
Self::SERVER_MISCONFIGURED
})?;
info!(?subject, ?user_id, "Shadow user created for {user_id}");
true
},
| AccountStatus::Deactivated => {
return Err("Your account has been deactivated.");
},
};
self.link_user(&user_id, subject);
// Import profile fields
if matches!(config.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin)
|| (matches!(
config.profile_key_import_mode,
OidcProfileKeyImportMode::OnRegistration
) && new_account_registered)
{
if let Some(email_claim) = &config.email_claim {
if let Some(email) = claims.email().map(|email| email.as_str())
&& let Ok(address) = Address::from_str(email)
{
if let Err(err) = self
.services
.threepid
.associate_localpart_email(user_id.localpart(), &address)
.await
{
warn!(?email_claim, ?address, "Failed to associate email address: {err}");
}
} else {
warn!(
?email_claim,
"Email claim was not present or was not a valid email address"
);
}
}
let user_id = user_id.clone();
let subject = claims.subject().to_string();
let profile_key_map = config.profile_key_map.clone();
let openidsubject_currentpictureurl = self.db.openidsubject_currentpictureurl.clone();
let users = self.services.users.clone();
let media = self.services.media.clone();
let import_task = self.runtime.spawn(async move {
for (field, claim) in &profile_key_map {
let Some(value) = all_claims.get(claim).cloned() else {
warn!(?field, ?claim, "IDP provided no value for this mapped claim");
continue;
};
let value = if let Some(picture_url) = value.as_str()
&& field == ProfileFieldName::AvatarUrl.as_str()
&& openidsubject_currentpictureurl
.get(&subject)
.await
.deserialized::<String>()
.ok()
.is_none_or(|current_picture| current_picture != picture_url)
{
match media.download_media(picture_url).await {
| Ok((mxc, size)) => {
openidsubject_currentpictureurl.insert(&subject, picture_url);
info!(?picture_url, ?mxc, ?size, "Downloaded profile picture");
ProfileFieldValue::AvatarUrl(mxc)
},
| Err(err) => {
warn!(
?claim,
?picture_url,
"Failed to download profile picture: {err}"
);
continue;
},
}
} else {
match ProfileFieldValue::new(field, value.clone()) {
| Ok(value) => value,
| Err(err) => {
warn!(
?field,
?claim,
?value,
"Failed to parse claim value for profile field: {err}"
);
continue;
},
}
};
if let Err(err) = users
.set_profile_field(
&user_id,
ProfileFieldChange::Set(value),
PropagateTo::Unchanged,
)
.await
{
warn!(?field, ?claim, "Error while setting profile field: {err}");
}
}
info!("Profile import complete");
});
// Only wait for import to complete if this is a new account,
// so they see the correct profile information in the account panel
if new_account_registered {
let _ = import_task.await;
}
}
Ok(SessionCompletionStatus::Complete(user_id))
}
pub fn link_user(&self, user_id: &UserId, subject: &str) {
self.db
.openidsubject_localpart
.insert(subject, user_id.localpart());
}
pub fn unlink_user(&self, subject: &str) { self.db.openidsubject_localpart.remove(subject); }
}
-398
View File
@@ -1,398 +0,0 @@
use std::{
fmt::Debug,
net::{IpAddr, SocketAddr},
};
use conduwuit::{Err, Result, debug, debug_info, err, error, trace};
use futures::{FutureExt, TryFutureExt};
use hickory_resolver::{
net::{DnsError, NetError},
proto::rr::rdata::SRV,
};
use ipaddress::IPAddress;
use ruma::ServerName;
use super::{
cache::{CachedDest, CachedOverride, MAX_IPS},
fed::{FedDest, PortString, add_port_to_hostname, get_ip_with_port},
};
#[derive(Clone, Debug)]
pub(crate) struct ActualDest {
pub(crate) dest: FedDest,
pub(crate) host: String,
}
impl ActualDest {
#[inline]
pub(crate) fn string(&self) -> String { self.dest.https_string() }
}
impl super::Service {
#[tracing::instrument(skip_all, level = "debug", name = "resolve")]
pub(crate) async fn get_actual_dest(&self, server_name: &ServerName) -> Result<ActualDest> {
let (CachedDest { dest, host, .. }, _cached) =
self.lookup_actual_dest(server_name).await?;
Ok(ActualDest { dest, host })
}
pub(crate) async fn lookup_actual_dest(
&self,
server_name: &ServerName,
) -> Result<(CachedDest, bool)> {
if let Ok(result) = self.cache.get_destination(server_name).await {
return Ok((result, true));
}
let _dedup = self.resolving.lock(server_name.as_str());
if let Ok(result) = self.cache.get_destination(server_name).await {
return Ok((result, true));
}
self.resolve_actual_dest(server_name, true)
.inspect_ok(|result| self.cache.set_destination(server_name, result))
.map_ok(|result| (result, false))
.boxed()
.await
}
/// Returns: `actual_destination`, host header
/// Implemented according to the specification at <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
/// Numbers in comments below refer to bullet points in linked section of
/// specification
#[tracing::instrument(name = "actual", level = "debug", skip(self, cache))]
pub async fn resolve_actual_dest(
&self,
dest: &ServerName,
cache: bool,
) -> Result<CachedDest> {
self.validate_dest(dest)?;
let mut host = dest.as_str().to_owned();
let actual_dest = match get_ip_with_port(dest.as_str()) {
| Some(host_port) => Self::actual_dest_1(host_port)?,
| None =>
if let Some(pos) = dest.as_str().find(':') {
self.actual_dest_2(dest, cache, pos).await?
} else {
self.services.server.check_running()?;
match self.request_well_known(dest.as_str()).await? {
| Some(delegated) =>
self.actual_dest_3(&mut host, cache, delegated).await?,
| _ => match self.query_srv_record(dest.as_str()).await? {
| Some(overrider) =>
self.actual_dest_4(&host, cache, overrider).await?,
| _ => self.actual_dest_5(dest, cache).await?,
},
}
},
};
// Can't use get_ip_with_port here because we don't want to add a port
// to an IP address if it wasn't specified
let host = if let Ok(addr) = host.parse::<SocketAddr>() {
FedDest::Literal(addr)
} else if let Ok(addr) = host.parse::<IpAddr>() {
FedDest::Named(addr.to_string(), FedDest::default_port())
} else if let Some(pos) = host.find(':') {
let (host, port) = host.split_at(pos);
FedDest::Named(
host.to_owned(),
port.try_into().unwrap_or_else(|_| FedDest::default_port()),
)
} else {
FedDest::Named(host, FedDest::default_port())
};
debug!("Actual destination: {actual_dest:?} hostname: {host:?}");
Ok(CachedDest {
dest: actual_dest,
host: host.uri_string(),
expire: CachedDest::default_expire(),
})
}
fn actual_dest_1(host_port: FedDest) -> Result<FedDest> {
debug!("1: IP literal with provided or default port");
Ok(host_port)
}
async fn actual_dest_2(&self, dest: &ServerName, cache: bool, pos: usize) -> Result<FedDest> {
debug!("2: Hostname with included port");
let (host, port) = dest.as_str().split_at(pos);
self.conditional_query_and_cache(host, port.parse::<u16>().unwrap_or(8448), cache)
.await?;
Ok(FedDest::Named(
host.to_owned(),
port.try_into().unwrap_or_else(|_| FedDest::default_port()),
))
}
async fn actual_dest_3(
&self,
host: &mut String,
cache: bool,
delegated: String,
) -> Result<FedDest> {
debug!("3: A .well-known file is available");
*host = add_port_to_hostname(&delegated).uri_string();
match get_ip_with_port(&delegated) {
| Some(host_and_port) => Self::actual_dest_3_1(host_and_port),
| None =>
if let Some(pos) = delegated.find(':') {
self.actual_dest_3_2(cache, delegated, pos).await
} else {
trace!("Delegated hostname has no port in this branch");
match self.query_srv_record(&delegated).await? {
| Some(overrider) =>
self.actual_dest_3_3(cache, delegated, overrider).await,
| _ => self.actual_dest_3_4(cache, delegated).await,
}
},
}
}
fn actual_dest_3_1(host_and_port: FedDest) -> Result<FedDest> {
debug!("3.1: IP literal in .well-known file");
Ok(host_and_port)
}
async fn actual_dest_3_2(
&self,
cache: bool,
delegated: String,
pos: usize,
) -> Result<FedDest> {
debug!("3.2: Hostname with port in .well-known file");
let (host, port) = delegated.split_at(pos);
self.conditional_query_and_cache(host, port.parse::<u16>().unwrap_or(8448), cache)
.await?;
Ok(FedDest::Named(
host.to_owned(),
port.try_into().unwrap_or_else(|_| FedDest::default_port()),
))
}
async fn actual_dest_3_3(
&self,
cache: bool,
delegated: String,
overrider: FedDest,
) -> Result<FedDest> {
debug!("3.3: SRV lookup successful");
let force_port = overrider.port();
self.conditional_query_and_cache_override(
&delegated,
&overrider.hostname(),
force_port.unwrap_or(8448),
cache,
)
.await?;
if let Some(port) = force_port {
return Ok(FedDest::Named(
delegated,
format!(":{port}")
.as_str()
.try_into()
.unwrap_or_else(|_| FedDest::default_port()),
));
}
Ok(add_port_to_hostname(&delegated))
}
async fn actual_dest_3_4(&self, cache: bool, delegated: String) -> Result<FedDest> {
debug!("3.4: No SRV records, just use the hostname from .well-known");
self.conditional_query_and_cache(&delegated, 8448, cache)
.await?;
Ok(add_port_to_hostname(&delegated))
}
async fn actual_dest_4(
&self,
host: &str,
cache: bool,
overrider: FedDest,
) -> Result<FedDest> {
debug!("4: No .well-known; SRV record found");
let force_port = overrider.port();
self.conditional_query_and_cache_override(
host,
&overrider.hostname(),
force_port.unwrap_or(8448),
cache,
)
.await?;
if let Some(port) = force_port {
let port = format!(":{port}");
return Ok(FedDest::Named(
host.to_owned(),
PortString::from(port.as_str()).unwrap_or_else(|_| FedDest::default_port()),
));
}
Ok(add_port_to_hostname(host))
}
async fn actual_dest_5(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
debug!("5: No SRV record found");
self.conditional_query_and_cache(dest.as_str(), 8448, cache)
.await?;
Ok(add_port_to_hostname(dest.as_str()))
}
#[inline]
async fn conditional_query_and_cache(
&self,
hostname: &str,
port: u16,
cache: bool,
) -> Result {
self.conditional_query_and_cache_override(hostname, hostname, port, cache)
.await
}
#[inline]
async fn conditional_query_and_cache_override(
&self,
untername: &str,
hostname: &str,
port: u16,
cache: bool,
) -> Result {
if !cache {
return Ok(());
}
if self.cache.has_override(untername).await {
return Ok(());
}
self.query_and_cache_override(untername, hostname, port)
.await
}
#[tracing::instrument(name = "ip", level = "debug", skip(self))]
async fn query_and_cache_override(
&self,
untername: &'_ str,
hostname: &'_ str,
port: u16,
) -> Result {
self.services.server.check_running()?;
debug!("querying IP for {untername:?} ({hostname:?}:{port})");
match self.resolver.resolver.lookup_ip(hostname.to_owned()).await {
| Err(e) => Self::handle_resolve_error(&e, hostname),
| Ok(override_ip) => {
self.cache.set_override(untername, &CachedOverride {
ips: override_ip.iter().take(MAX_IPS).collect(),
port,
expire: CachedOverride::default_expire(),
overriding: (hostname != untername)
.then_some(hostname.into())
.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
});
Ok(())
},
}
}
#[tracing::instrument(name = "srv", level = "debug", skip(self))]
async fn query_srv_record(&self, hostname: &'_ str) -> Result<Option<FedDest>> {
let hostnames =
[format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")];
for hostname in hostnames {
self.services.server.check_running()?;
debug!("querying SRV for {hostname:?}");
let hostname = hostname.trim_end_matches('.');
match self.resolver.resolver.srv_lookup(hostname).await {
| Err(e) => Self::handle_resolve_error(&e, hostname)?,
| Ok(result) => {
return Ok(result.answers().iter().next().map(|result| {
let data = result.try_borrow::<SRV>().expect("should be SRV response");
FedDest::Named(
data.data()
.target
.to_string()
.trim_end_matches('.')
.to_owned(),
format!(":{}", data.data().port)
.as_str()
.try_into()
.unwrap_or_else(|_| FedDest::default_port()),
)
}));
},
}
}
Ok(None)
}
fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> {
match err {
| NetError::NoConnections => {
error!(
"Your DNS server is overloaded and has ran out of connections. It is \
strongly recommended you remediate this issue to ensure proper federation \
connectivity."
);
Err!(error!(%host, "DNS error: {err}"))
},
| NetError::Timeout => Err!(error!(%host, "DNS query timed out")),
| NetError::Dns(DnsError::NoRecordsFound(..)) => {
// Raise to debug_warn if we can find out the result wasn't from cache
debug!(%host, "No DNS records found: {err}");
Ok(())
},
| _ => Err!(error!(%host, "DNS error: {err}")),
}
}
fn validate_dest(&self, dest: &ServerName) -> Result<()> {
if dest == self.services.server.name && !self.services.server.config.federation_loopback {
return Err!("Won't send federation request to ourselves");
}
if dest.is_ip_literal() || IPAddress::is_valid(dest.host()) {
self.validate_dest_ip_literal(dest)?;
}
Ok(())
}
fn validate_dest_ip_literal(&self, dest: &ServerName) -> Result<()> {
trace!("Destination is an IP literal, checking against IP range denylist.",);
debug_assert!(
dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
"Destination is not an IP literal."
);
let ip = IPAddress::parse(dest.host()).map_err(|e| {
err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}")))
})?;
self.validate_ip(&ip)?;
Ok(())
}
pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
if !self.services.client.valid_cidr_range(ip) {
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
}
Ok(())
}
}
+1 -11
View File
@@ -4,7 +4,7 @@
Result,
arrayvec::ArrayVec,
at, err,
utils::{math::Expected, rand, stream::TryIgnore},
utils::{math::Expected, stream::TryIgnore},
};
use database::{Cbor, Deserialized, Map};
use futures::{Stream, StreamExt, future::join};
@@ -114,11 +114,6 @@ impl CachedDest {
#[must_use]
pub fn valid(&self) -> bool { self.expire > SystemTime::now() }
#[must_use]
pub(crate) fn default_expire() -> SystemTime {
rand::time_from_now_secs(60 * 60 * 18..60 * 60 * 36)
}
#[inline]
#[must_use]
pub fn size(&self) -> usize {
@@ -134,11 +129,6 @@ impl CachedOverride {
#[must_use]
pub fn valid(&self) -> bool { self.expire > SystemTime::now() }
#[must_use]
pub(crate) fn default_expire() -> SystemTime {
rand::time_from_now_secs(60 * 60 * 6..60 * 60 * 12)
}
#[inline]
#[must_use]
pub fn size(&self) -> usize { size_of_val(self) }
+3 -53
View File
@@ -1,16 +1,12 @@
use std::{
borrow::Cow,
fmt,
net::{IpAddr, SocketAddr},
};
use std::{fmt, net::SocketAddr};
use conduwuit::{arrayvec::ArrayString, utils::math::Expected};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub enum FedDest {
Literal(SocketAddr),
Named(String, PortString),
Literal(SocketAddr), // "ip:port"
Named(String, PortString), // ("hostname", ":port")
}
/// numeric or service-name
@@ -18,36 +14,7 @@ pub enum FedDest {
const DEFAULT_PORT: &str = ":8448";
pub(crate) fn get_ip_with_port(dest_str: &str) -> Option<FedDest> {
if let Ok(dest) = dest_str.parse::<SocketAddr>() {
Some(FedDest::Literal(dest))
} else if let Ok(ip_addr) = dest_str.parse::<IpAddr>() {
Some(FedDest::Literal(SocketAddr::new(ip_addr, 8448)))
} else {
None
}
}
pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {
let (host, port) = match dest.find(':') {
| None => (dest, DEFAULT_PORT),
| Some(pos) => dest.split_at(pos),
};
FedDest::Named(
host.to_owned(),
PortString::from(port).unwrap_or_else(|_| FedDest::default_port()),
)
}
impl FedDest {
pub(crate) fn https_string(&self) -> String {
match self {
| Self::Literal(addr) => format!("https://{addr}"),
| Self::Named(host, port) => format!("https://{host}{port}"),
}
}
pub(crate) fn uri_string(&self) -> String {
match self {
| Self::Literal(addr) => addr.to_string(),
@@ -55,23 +22,6 @@ pub(crate) fn uri_string(&self) -> String {
}
}
#[inline]
pub(crate) fn hostname(&self) -> Cow<'_, str> {
match &self {
| Self::Literal(addr) => addr.ip().to_string().into(),
| Self::Named(host, _) => host.into(),
}
}
#[inline]
#[allow(clippy::string_slice)]
pub(crate) fn port(&self) -> Option<u16> {
match &self {
| Self::Literal(addr) => Some(addr.port()),
| Self::Named(_, port) => port[1..].parse().ok(),
}
}
#[inline]
#[must_use]
pub fn default_port() -> PortString {
+34 -24
View File
@@ -1,54 +1,64 @@
pub mod actual;
pub mod cache;
mod dns;
pub mod fed;
#[cfg(test)]
mod tests;
mod well_known;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{Result, Server, arrayvec::ArrayString, utils::MutexMap};
use conduwuit::Result;
use reqwest::redirect;
use resolvematrix::server::{MatrixResolver, MatrixResolverBuilder};
use self::{cache::Cache, dns::Resolver};
use crate::{Dep, client};
use crate::client::base;
pub struct Service {
pub cache: Arc<Cache>,
pub resolver: Arc<Resolver>,
resolving: Resolving,
pub resolver: MatrixResolver,
pub dns: Dns,
#[allow(dead_code)] // This service doesn't access services after construction
services: Services,
}
struct Services {
server: Arc<Server>,
client: Dep<client::Service>,
}
struct Services;
type Resolving = MutexMap<NameBuf, ()>;
type NameBuf = ArrayString<256>;
pub struct Dns {
pub cache: Arc<Cache>,
pub resolver: Arc<Resolver>,
}
#[async_trait]
impl crate::Service for Service {
#[allow(clippy::as_conversions, clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
let cache = Cache::new(&args);
let resolver = Resolver::build(args.server, cache.clone())?;
Ok(Arc::new(Self {
cache: cache.clone(),
resolver: Resolver::build(args.server, cache)?,
resolving: MutexMap::new(),
services: Services {
server: args.server.clone(),
client: args.depend::<client::Service>("client"),
resolver: MatrixResolverBuilder::new()
.dangerous_tls_accept_invalid_certs(args.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
.http_client(
base(&args.server.config)?
.connect_timeout(Duration::from_secs(args.server.config.well_known_conn_timeout))
.read_timeout(Duration::from_secs(args.server.config.well_known_timeout))
.timeout(Duration::from_secs(args.server.config.well_known_timeout))
.pool_max_idle_per_host(0)
.redirect(redirect::Policy::limited(4))
.build()?
)
.dns_resolver(resolver.resolver.clone())
.build()?,
dns: Dns {
cache,
resolver,
},
services: Services {},
}))
}
async fn clear_cache(&self) {
self.resolver.clear_cache();
self.cache.clear().await;
self.dns.resolver.clear_cache();
self.dns.cache.clear().await;
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
fn name(&self) -> &str { crate::service::make_name(module_path!()) }
}
-41
View File
@@ -1,41 +0,0 @@
use super::fed::{FedDest, add_port_to_hostname, get_ip_with_port};
#[test]
fn ips_get_default_ports() {
assert_eq!(
get_ip_with_port("1.1.1.1"),
Some(FedDest::Literal("1.1.1.1:8448".parse().unwrap()))
);
assert_eq!(
get_ip_with_port("dead:beef::"),
Some(FedDest::Literal("[dead:beef::]:8448".parse().unwrap()))
);
}
#[test]
fn ips_keep_custom_ports() {
assert_eq!(
get_ip_with_port("1.1.1.1:1234"),
Some(FedDest::Literal("1.1.1.1:1234".parse().unwrap()))
);
assert_eq!(
get_ip_with_port("[dead::beef]:8933"),
Some(FedDest::Literal("[dead::beef]:8933".parse().unwrap()))
);
}
#[test]
fn hostnames_get_default_ports() {
assert_eq!(
add_port_to_hostname("example.com"),
FedDest::Named(String::from("example.com"), ":8448".try_into().unwrap())
);
}
#[test]
fn hostnames_keep_custom_ports() {
assert_eq!(
add_port_to_hostname("example.com:1337"),
FedDest::Named(String::from("example.com"), ":1337".try_into().unwrap())
);
}
-50
View File
@@ -1,50 +0,0 @@
use conduwuit::{Result, debug, debug_error, debug_info, trace, utils::response::LimitReadExt};
use ruma::ServerName;
impl super::Service {
#[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))]
pub(super) async fn request_well_known(&self, dest: &str) -> Result<Option<String>> {
trace!("Requesting well known for {dest}");
let response = self
.services
.client
.well_known
.get(format!("https://{dest}/.well-known/matrix/server"))
.send()
.await;
trace!("response: {response:?}");
if let Err(e) = &response {
debug!("error: {e:?}");
return Ok(None);
}
let response = response?;
if !response.status().is_success() {
debug!("response not 2XX");
return Ok(None);
}
let Ok(text) = response.limit_read_text(8192).await else {
debug!("failed to read well-known response (too large or non-text content)");
return Ok(None);
};
trace!("response text: {text:?}");
let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
let m_server = body
.get("m.server")
.unwrap_or(&serde_json::Value::Null)
.as_str()
.unwrap_or_default();
if ServerName::parse(m_server).is_err() {
debug_error!("response content missing or invalid");
return Ok(None);
}
debug_info!("{dest:?} found at {m_server:?}");
Ok(Some(m_server.to_owned()))
}
}
@@ -1,235 +1,661 @@
use std::{
collections::{BTreeMap, HashSet, VecDeque, hash_map},
collections::{HashMap, HashSet, VecDeque},
time::Instant,
};
use assign::assign;
#[cfg(debug_assertions)]
use conduwuit::error;
use conduwuit::{
Event, PduEvent, debug, debug_warn, matrix::event::gen_event_id_canonical_json, trace,
utils::continue_exponential_backoff_secs, warn,
Err, Event, PduEvent, debug, debug_error, debug_info, debug_warn, err,
result::FlatOk,
state_res::lexicographical_topological_sort,
trace,
utils::{IterStream, math::Expected, stream::BroadbandExt},
warn,
};
use futures::{StreamExt, future::select_ok};
use ruma::{
CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
api::federation::event::get_event,
CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId,
OwnedServerName, RoomId, ServerName, UInt,
api::federation::event::{get_event, get_missing_events},
int,
room_version_rules::RoomVersionRules,
};
use super::get_room_version_rules;
use crate::rooms::event_handler::parse_incoming_pdu::expect_event_id_array;
pub const GET_MISSING_EVENTS_MAX_BATCH_SIZE: usize = 50;
/// 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.
#[allow(clippy::implicit_hasher)]
pub async fn build_local_dag(
pdu_map: &HashMap<OwnedEventId, &CanonicalJsonObject>,
) -> conduwuit::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::with_capacity(pdu_map.len());
let mut id_origin_ts: HashMap<OwnedEventId, _> = HashMap::with_capacity(pdu_map.len());
for (event_id, value) in pdu_map {
// Parse all prev events as event IDs - if they are missing, return an error (we
// can't sanely continue in this case), otherwise skip invalid prev events.
let prev_events = value
.get("prev_events")
.and_then(CanonicalJsonValue::as_array)
.ok_or_else(|| {
err!(Request(BadJson("event JSON for {event_id} is missing prev_events")))
})?
.iter()
.map(|v| v.as_str().and_then(|s| EventId::parse(s).ok()))
.filter(|id| id.as_ref().is_some_and(|id| pdu_map.contains_key(id)))
.map(Option::unwrap)
.collect();
dag.insert(event_id.clone(), prev_events);
let origin_server_ts = value
.get("origin_server_ts")
.and_then(CanonicalJsonValue::as_integer)
.map(i64::from)
.map(UInt::try_from)
.flat_ok()
.unwrap_or_default();
id_origin_ts.insert(event_id.clone(), origin_server_ts);
}
debug!(count = dag.len(), "Sorting incoming events with partial graph");
lexicographical_topological_sort(&dag, &async |node_id| {
// 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.
let ts = id_origin_ts.get(&node_id).copied().unwrap_or_default();
Ok((int!(0), MilliSecondsSinceUnixEpoch(ts)))
})
.await
.inspect(|sorted| {
debug_assert_eq!(
sorted.len(),
pdu_map.len(),
"Sorted graph was not the same size as the input graph"
);
})
.map_err(|e| err!("failed to resolve local graph: {e}"))
}
impl super::Service {
/// Find the event and auth it. Once the event is validated (steps 1 - 8)
/// it is appended to the outliers Tree.
/// Uses `POST /_matrix/federation/v1/get_missing_events/{room_id}` to fill
/// gaps in the DAG.
///
/// Returns pdu and if we fetched it over federation the raw json.
/// This function walks backwards from `head`, fetching incrementally (by a
/// factor of 10) more events until the remote we're fetching from either
/// stops returning new events, or the min_depth is reached.
///
/// a. Look in the main timeline (pduid_pdu tree)
/// b. Look at outlier pdu tree
/// c. Ask origin server over federation
/// d. TODO: Ask other servers over federation?
pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
/// This function does not persist the events, but does validate them. The
/// caller is responsible for passing them through handle_incoming_pdu or
/// related functions.
///
/// Only the one `via` is asked for missing events, as multiplexing remotes
/// may result in the event tree being walked in a gappy or disordered
/// manner.
///
/// ## Parameters
///
/// - `room_id`: The room's ID.
/// - `head`: The event we are potentially missing prev_events for.
/// - `tail`: The most recently known events in the graph (typically forward
/// extremities).
/// - `via`: The server to ask for missing events.
/// - `min_depth`: Don't process events with a `depth` lower than this
/// value. Not massively useful, but can help short-circuit infinite loops
/// and weird edge paths.
#[tracing::instrument(name = "get_missing_events_bulk", skip_all)]
pub async fn get_missing_events(
&self,
origin: &'a ServerName,
events: Events,
create_event: &'a Pdu,
room_id: &'a RoomId,
) -> Vec<(PduEvent, Option<BTreeMap<String, CanonicalJsonValue>>)>
where
Pdu: Event + Send + Sync,
Events: Iterator<Item = &'a EventId> + Clone + Send,
{
let back_off = |id| match self
.services
.globals
.bad_event_ratelimiter
.write()
.entry(id)
room_id: &RoomId,
head: &PduEvent,
tail: Vec<OwnedEventId>,
via: &ServerName,
min_depth: UInt,
) -> conduwuit::Result<HashMap<OwnedEventId, PduEvent>> {
let start = Instant::now();
#[cfg(debug_assertions)]
{
| hash_map::Entry::Vacant(e) => {
e.insert((Instant::now(), 1));
},
| hash_map::Entry::Occupied(mut e) => {
*e.get_mut() = (Instant::now(), e.get().1.saturating_add(1));
},
};
let mut events_with_auth_events = Vec::with_capacity(events.clone().count());
trace!("Fetching {} outlier pdus", events.clone().count());
for id in events {
// a. Look in the main timeline (pduid_pdu tree)
// b. Look at outlier pdu tree
// (get_pdu_json checks both)
if let Ok(local_pdu) = self.services.timeline.get_pdu(id).await {
trace!("Found {id} in main timeline or outlier tree");
events_with_auth_events.push((id.to_owned(), Some(local_pdu), vec![]));
continue;
}
// c. Ask origin server over federation
// We also handle its auth chain here so we don't get a stack overflow in
// handle_outlier_pdu.
let mut todo_auth_events: VecDeque<_> = [id.to_owned()].into();
let mut events_in_reverse_order = Vec::with_capacity(todo_auth_events.len());
let mut events_all = HashSet::with_capacity(todo_auth_events.len());
while let Some(next_id) = todo_auth_events.pop_front() {
if let Some((time, tries)) = self
.services
.globals
.bad_event_ratelimiter
.read()
.get(&*next_id)
{
// Exponential backoff
const MIN_DURATION: u64 = 60 * 2;
const MAX_DURATION: u64 = 60 * 60 * 8;
if continue_exponential_backoff_secs(
MIN_DURATION,
MAX_DURATION,
time.elapsed(),
*tries,
) {
debug_warn!(
tried = ?*tries,
elapsed = ?time.elapsed(),
"Backing off from {next_id}",
);
continue;
let missing_count = head
.prev_events()
.stream()
.fold(0_u8, |i, event_id| async move {
if self.services.timeline.pdu_exists(event_id).await {
i.expected_add(1)
} else {
i
}
}
})
.await;
debug_assert_ne!(
missing_count, 0,
"event passed to get_missing_events is not missing any events (wasteful call)"
);
};
assert!(!tail.is_empty(), "empty tail");
assert_ne!(via, self.services.globals.server_name(), "cannot ask ourselves for events");
if events_all.contains(&next_id) {
// The iteration limit is in place to ensure that if the remote server leaves us
// in a state of infinite recursion (as old versions of continuwuity and
// predecessors would), we give up. However, get_missing_events doesn't return
// that many events per-request. Synapse returns 20, and conduwuit+ return 50.
// This means with a hard iteration limit, we might give up too early, before
// we get a chance to even come close to max_fetch_prev_events. As such, we'll
// calculate the limit based on that config option and the aforementioned
// averages.
let max_fetch = self.services.server.config.max_fetch_prev_events;
let iteration_limit = max_fetch.saturating_div(20).max(10);
let mut discovered = HashMap::with_capacity(head.prev_events.len());
let mut latest_events: Vec<OwnedEventId> = vec![head.event_id().to_owned()];
debug!(elapsed=?start.elapsed(),
%room_id,
event_id=%head.event_id(),
%iteration_limit,
"Fetching any missing events for head event",
);
for iteration in 0..iteration_limit {
let limit = iteration
.expected_add(1)
.saturating_mul(10)
.min(GET_MISSING_EVENTS_MAX_BATCH_SIZE.try_into().expect(
"GET_MISSING_EVENTS_MAX_BATCH_SIZE (usize) should fit in u16 (<=65536)",
))
.max(
// This max call ensures we fetch *at least* all the prev events the
// head has.
u16::try_from(head.prev_events.len())
.expect("cannot have more than 20 prev events, which fits in u16"),
);
debug_info!(elapsed=?start.elapsed(),
%limit,
%via,
%iteration,
%iteration_limit,
discovered=discovered.len(),
%min_depth,
"Attempting to gap fill missing events"
);
let response: get_missing_events::v1::Response = self
.services
.sending
.send_federation_request(
via,
assign!(
get_missing_events::v1::Request::new(
room_id.to_owned(),
tail.clone(),
latest_events.clone()
),
{limit: limit.into(), min_depth}
),
)
.await?;
if response.events.is_empty() {
debug_info!(
elapsed=?start.elapsed(),
%via,
"Finished gap filling missing events (remote returned no more events)."
);
break;
}
debug_info!(
elapsed=?start.elapsed(),
"Got {} events back from remote",
response.events.len()
);
latest_events.clear();
for raw_event in response.events {
let (_, event_id, pdu_json) = self.parse_incoming_pdu(&raw_event).await?;
let pdu = PduEvent::from_id_val(&event_id, pdu_json).map_err(|e| {
err!(Request(BadJson("Failed to parse gapfilled event {event_id}: {e}")))
})?;
if discovered.contains_key(&event_id) {
// We already received this event.
trace!("Already received {event_id}");
continue;
}
if self.services.timeline.pdu_exists(&next_id).await {
trace!("Found {next_id} in db");
continue;
}
debug!("Fetching {next_id} over federation from {origin}.");
match self
if self
.services
.sending
.send_federation_request(
origin,
get_event::v1::Request::new((*next_id).to_owned()),
)
.timeline
.non_outlier_pdu_exists(&event_id)
.await
{
| Ok(res) => {
debug!("Got {next_id} over federation from {origin}");
let Ok(room_version_rules) = get_room_version_rules(create_event) else {
back_off((*next_id).to_owned());
continue;
};
let Ok((calculated_event_id, value)) =
gen_event_id_canonical_json(&res.pdu, &room_version_rules)
else {
back_off((*next_id).to_owned());
continue;
};
if calculated_event_id != *next_id {
warn!(
"Server didn't return event id we requested: requested: \
{next_id}, we got {calculated_event_id}. Event: {:?}",
&res.pdu
);
}
if let Some(auth_events) = value
.get("auth_events")
.and_then(CanonicalJsonValue::as_array)
{
for auth_event in auth_events {
match serde_json::from_value::<OwnedEventId>(
auth_event.clone().into(),
) {
| Ok(auth_event) => {
trace!(
"Found auth event id {auth_event} for event \
{next_id}"
);
todo_auth_events.push_back(auth_event);
},
| _ => {
warn!("Auth event id is not valid");
},
}
}
} else {
warn!("Auth event list invalid");
}
events_in_reverse_order.push((next_id.clone(), value));
events_all.insert(next_id);
},
| Err(e) => {
warn!("Failed to fetch auth event {next_id} from {origin}: {e}");
back_off((*next_id).to_owned());
},
// NOTE: we explicitly check for *non*-outlier events here, as if we end
// up discovering outlier events, we will be able to upgrade them
// immediately.
trace!("Already have {event_id} as a timeline PDU");
continue;
}
}
events_with_auth_events.push((id.to_owned(), None, events_in_reverse_order));
}
if pdu.depth < min_depth {
debug_warn!(
elapsed=?start.elapsed(),
"Received PDU with depth {} below min_depth {}",
pdu.depth,
min_depth
);
discovered.insert(event_id.clone(), pdu);
continue;
}
let mut pdus = Vec::with_capacity(events_with_auth_events.len());
for (id, local_pdu, events_in_reverse_order) in events_with_auth_events {
// a. Look in the main timeline (pduid_pdu tree)
// b. Look at outlier pdu tree
// (get_pdu_json checks both)
if let Some(local_pdu) = local_pdu {
trace!("Found {id} in main timeline or outlier tree");
pdus.push((local_pdu.clone(), None));
}
for (next_id, value) in events_in_reverse_order.into_iter().rev() {
if let Some((time, tries)) = self
.services
.globals
.bad_event_ratelimiter
.read()
.get(&*next_id)
{
// Exponential backoff
const MIN_DURATION: u64 = 5 * 60;
const MAX_DURATION: u64 = 60 * 60 * 24;
if continue_exponential_backoff_secs(
MIN_DURATION,
MAX_DURATION,
time.elapsed(),
*tries,
) {
debug!("Backing off from {next_id}");
for prev_event_id in pdu.prev_events() {
if discovered.contains_key(prev_event_id) {
// We already received this event.
trace!("Already received prev event {prev_event_id}");
continue;
}
if self
.services
.timeline
.non_outlier_pdu_exists(prev_event_id)
.await
{
// NOTE: we explicitly check for *non*-outlier events here, as if we end
// up discovering outlier events, we will be able to upgrade them
// immediately.
trace!("Already have prev event {prev_event_id} as a timeline PDU");
continue;
}
if let Ok(outlier) = self.services.timeline.get_pdu(prev_event_id).await {
// We already have this PDU as an outlier, don't ask for
// it. However, if we are missing any prev events for it, add it to the
// latest events anyway.
let outlier_missing_prevs = outlier
.prev_events()
.stream()
.fold(0_u8, |i, event_id| async move {
if self.services.timeline.pdu_exists(event_id).await {
i.expected_add(1)
} else {
i
}
})
.await;
if outlier_missing_prevs > 0 {
trace!("Missing {outlier_missing_prevs} PDU(s) for prev event");
latest_events.push(prev_event_id.to_owned());
}
trace!("Had {prev_event_id} as an outlier already, skipping discovery");
discovered.insert(prev_event_id.to_owned(), outlier);
continue;
}
trace!("Missing prev {prev_event_id} of {event_id}");
latest_events.push(prev_event_id.to_owned());
}
trace!("Discovered {event_id}");
discovered.insert(event_id.clone(), pdu);
}
if latest_events.is_empty() {
debug!(elapsed=?start.elapsed(),
%limit,
%via,
%iteration,
discovered=discovered.len(),
"No more events to fetch."
);
break;
}
if discovered.len() >= self.services.server.config.max_fetch_prev_events.into() {
// Stupid hack, debug_error!() drops the log to a DEBUG when not in debug mode,
// which is bad because this should at least produce a warning. It's an error in
// debug mode because this can be important, but typically not much can be done
// about it as a user.
#[cfg(debug_assertions)]
error!(elapsed=?start.elapsed(),
discovered=discovered.len(),
max_fetch_prev_events=self.services.server.config.max_fetch_prev_events,
%iteration,
%iteration_limit,
%via,
event_id=%head.event_id(),
%room_id,
"Encountered a gap too large to fill, giving up"
);
#[cfg(not(debug_assertions))]
warn!(elapsed=?start.elapsed(),
discovered=discovered.len(),
max_fetch_prev_events=self.services.server.config.max_fetch_prev_events,
%iteration,
%iteration_limit,
%via,
event_id=%head.event_id(),
%room_id,
"Encountered a gap too large to fill"
);
break;
}
}
trace!(elapsed=?start.elapsed(), "Finished get_missing_events");
Ok(discovered)
}
/// Sends a `GET /_matrix/federation/v1/event/{event_id}` request to the
/// target `remote`, parses the resulting PDU, and ensures the remote
/// returned the correct event.
/// Allows `fetch_and_handle_missing_events` to atomically fetch events from
/// multiple remotes in parallel.
async fn fetch_event_via(
&self,
remote: OwnedServerName,
event_id: OwnedEventId,
room_version_rules: &RoomVersionRules,
) -> conduwuit::Result<(OwnedEventId, CanonicalJsonObject)> {
let res = self
.services
.sending
.send_federation_request(&remote, get_event::v1::Request::new(event_id.clone()))
.await?;
let (calculated_event_id, value) = self
.parse_incoming_pdu_with_known_room(&res.pdu, room_version_rules)
.await?;
if calculated_event_id != event_id {
Err!(Request(BadJson(warn!(
expected=%event_id,
received=%calculated_event_id,
"Server didn't return event id we requested",
))))
} else {
Ok((event_id, value))
}
}
async fn fetch_event_vias(
&self,
candidates: impl Iterator<Item = &OwnedServerName>,
event_id: &EventId,
room_version_rules: &RoomVersionRules,
) -> conduwuit::Result<(OwnedEventId, CanonicalJsonObject)> {
if let Ok(pdu_json) = self.services.timeline.get_pdu_json(event_id).await {
return Ok((event_id.to_owned(), pdu_json));
}
let futures = candidates
.map(|remote| {
Box::pin(self.fetch_event_via(
remote.to_owned(),
event_id.to_owned(),
room_version_rules,
))
})
.collect::<Vec<_>>();
select_ok(futures).await.map(|(res, _)| res)
}
/// Asks remote servers for any individual events that are missing, also
/// known as "atomic fetch". Should only be used for fetching missing auth
/// events or resolving missing events from state_ids. For all other uses,
/// use get_missing_events.
///
/// This function manually walks auth_events trees in a breadth-first
/// search, and persists all fetched events as outliers when all the
/// backwards extremities have been resolved.
#[tracing::instrument(name = "get_missing_auth_events_atomic", skip_all)]
pub(super) async fn fetch_and_handle_auth_events<Pdu>(
&self,
origin: &ServerName,
events: Vec<OwnedEventId>,
create_event: &Pdu,
room_id: &RoomId,
) -> HashMap<OwnedEventId, PduEvent>
where
Pdu: Event + Send + Sync,
{
let start = Instant::now();
let room_version_rules =
&get_room_version_rules(create_event).unwrap_or(RoomVersionRules::V1);
let mut candidates = self
.services
.timeline
.candidate_backfill_servers(room_id)
.await;
candidates.insert(origin.to_owned());
assert!(!candidates.is_empty(), "no candidates to fetch missing events from");
let mut discovered_events =
HashMap::with_capacity(events.len().saturating_add(events.len().saturating_mul(3)));
trace!(
elapsed=?start.elapsed(),
"Fetching {} unknown PDUs on demand from {} candidates",
events.len(),
candidates.len()
);
let mut seen: HashMap<OwnedEventId, u8> = HashMap::new();
for apex_event_id in &events {
let mut todo: VecDeque<OwnedEventId> = [apex_event_id.to_owned()].into();
while let Some(target_id) = todo.pop_front() {
if discovered_events.contains_key(&target_id) {
continue;
}
if let Ok(local_pdu) = self.services.timeline.get_pdu(&target_id).await {
trace!(elapsed=?start.elapsed(), "Found {target_id} in db");
let mut obj = local_pdu.into_canonical_object();
obj.remove("event_id");
discovered_events.insert(target_id.clone(), obj);
continue;
}
let attempts = seen.get(&*target_id).copied().unwrap_or_default();
if attempts >= 5 {
debug_error!(
elapsed=?start.elapsed(),
%attempts,
%target_id,
"Could not fetch missing event after 5 attempts, giving up"
);
continue;
}
trace!("Handling outlier {next_id}");
match Box::pin(self.handle_outlier_pdu(
origin,
create_event,
&next_id,
room_id,
value.clone(),
true,
))
.await
debug!(elapsed=?start.elapsed(),"Fetching {target_id} over federation");
let value = match self
.fetch_event_vias(candidates.iter(), &target_id, room_version_rules)
.await
{
| Ok((pdu, json)) =>
if next_id == *id {
trace!("Handled outlier {next_id} (original request)");
pdus.push((pdu, Some(json)));
},
| Ok((_, x)) => x,
| Err(e) => {
warn!("Authentication of event {next_id} failed: {e:?}");
back_off(next_id);
warn!(elapsed=?start.elapsed(),"failed to fetch missing event {target_id} from any candidate: {e}");
continue;
},
};
let auth_events =
match expect_event_id_array(&value, "auth_events").map_err(|e| {
err!(Request(BadJson(warn!(
elapsed=?start.elapsed(),
event_id=%target_id,
"Failed to parse event fetched from remote: {e}"
))))
}) {
| Ok(auth_events) => auth_events,
| Err(e) => {
warn!(
elapsed=?start.elapsed(),
?e,
"event {target_id} is malformed (bad auth_events), skipping"
);
continue;
},
};
let mut have_all_auth = true;
for auth_event_id in auth_events {
if let Ok(local_pdu) = self.services.timeline.get_pdu(&auth_event_id).await {
trace!(elapsed=?start.elapsed(),"Found auth event {auth_event_id} in db");
let mut obj = local_pdu.into_canonical_object();
obj.remove("event_id");
discovered_events.insert(auth_event_id.clone(), obj);
continue;
}
if discovered_events.contains_key(&auth_event_id) {
trace!(elapsed=?start.elapsed(),%auth_event_id, "Already found auth event");
continue;
}
debug!(elapsed=?start.elapsed(),"Missing auth event {auth_event_id} for event {target_id}");
seen.insert(
auth_event_id.clone(),
seen.get(&auth_event_id)
.copied()
.unwrap_or_default()
.saturating_add(1),
);
todo.push_back(auth_event_id);
have_all_auth = false;
}
// Insert this PDU back at the end of the queue so that it will be resolved once
// all of its auth events have been fetched.
if have_all_auth {
debug!(elapsed=?start.elapsed(),%target_id, "Have all auth events");
discovered_events.insert(target_id, value);
} else {
debug_warn!(elapsed=?start.elapsed(),
"Fetched {target_id} but missing some auth events, will have to re-fetch."
);
seen.insert(target_id.clone(), attempts.saturating_add(1));
todo.push_back(target_id);
}
}
}
trace!("Fetched and handled {} outlier pdus", pdus.len());
let refmap: HashMap<OwnedEventId, &CanonicalJsonObject> = discovered_events
.iter()
.map(|(id, data)| (id.clone(), data))
.collect();
let seeded_ordered = build_local_dag(&refmap)
.await
.expect("failed to build local DAG");
let mut pdus = HashMap::with_capacity(seeded_ordered.len());
for discovered_event_id in seeded_ordered {
let pdu_json = discovered_events.remove(&discovered_event_id).unwrap();
debug_info!(
elapsed=?start.elapsed(),
"Handling missing event {discovered_event_id} as outlier"
);
assert_eq!(pdu_json.get("event_id"), None, "pdu_json had event_id");
match Box::pin(self.handle_outlier_pdu(
origin,
create_event,
&discovered_event_id,
room_id,
pdu_json,
))
.await
{
| Ok((pdu, _)) => {
trace!(elapsed=?start.elapsed(), "Persisted {discovered_event_id}");
let _ = pdus.insert(discovered_event_id, pdu);
},
| Err(e) => warn!(
elapsed=?start.elapsed(),
"Authentication of event {discovered_event_id} failed: {e:?}"
),
}
}
trace!(
elapsed=?start.elapsed(),
"Finished fetch_and_handle_missing_events: fetched and handled {} missing PDUs",
pdus.len()
);
pdus.retain(|id, _| events.contains(id)); // Only return state events
trace!(elapsed=?start.elapsed(), "Filtered return value down to {} PDUs", pdus.len());
pdus
}
/// Similar to `fetch_and_handle_missing_events`, but simply walks the
/// prev events tree instead of the auth events tree. Additionally, it does
/// not *handle* fetched PDUs in any capacity.
#[tracing::instrument(name = "get_missing_prev_events_atomic", skip_all)]
pub(super) async fn fetch_prev_events<Pdu>(
&self,
origin: &ServerName,
events: Vec<OwnedEventId>,
create_event: &Pdu,
room_id: &RoomId,
) -> HashMap<OwnedEventId, PduEvent>
where
Pdu: Event + Send + Sync,
{
let room_version_rules =
&get_room_version_rules(create_event).unwrap_or(RoomVersionRules::V1);
let mut candidates = self
.services
.timeline
.candidate_backfill_servers(room_id)
.await;
candidates.insert(origin.to_owned());
let mut todo: VecDeque<OwnedEventId> = VecDeque::from(events);
let mut discovered_events = HashMap::new();
while let Some(next_id) = todo.pop_front() {
if discovered_events.len() >= self.services.server.config.max_fetch_prev_events.into()
{
debug_warn!(
"Encountered a gap too large to fill, giving up (fetched {} events)",
discovered_events.len()
);
break;
}
if discovered_events.contains_key(&next_id) {
continue;
}
let pdu = match self
.fetch_event_vias(candidates.iter(), &next_id, room_version_rules)
.await
{
| Ok((_, data)) => data,
| Err(e) => {
warn!("Failed to fetch prev event {next_id} from any candidate: {e}");
continue;
},
};
let prev_events = match expect_event_id_array(&pdu, "prev_events").map_err(|e| {
err!(Request(BadJson(warn!(
event_id=%next_id,
"Failed to parse event fetched from remote: {e}"
))))
}) {
| Ok(auth_events) => auth_events,
| Err(e) => {
warn!(?e, "event {next_id} is malformed (bad prev_events), skipping");
continue;
},
};
let missing_prev = prev_events
.iter()
.stream()
.broad_filter_map(|event_id| async {
if discovered_events.contains_key(event_id)
|| self.services.timeline.pdu_exists(event_id).await
{
None
} else {
Some(event_id.to_owned())
}
})
.collect::<Vec<_>>()
.await;
todo.extend(missing_prev);
discovered_events.insert(
next_id.clone(),
PduEvent::from_id_val(&next_id, pdu).expect("fetched PDU was already validated"),
);
}
discovered_events
}
}
+134 -103
View File
@@ -1,124 +1,155 @@
use std::{
collections::{BTreeMap, HashMap, HashSet, VecDeque},
iter::once,
};
use std::{collections::HashMap, time::Instant};
use conduwuit::{
Event, PduEvent, Result, debug_warn, err,
state_res::{self},
};
use futures::{FutureExt, future};
use ruma::{
CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, ServerName,
int, uint,
Event, PduEvent, debug, debug_info, debug_warn, trace,
utils::{BoolExt, IterStream, stream::BroadbandExt},
};
use futures::StreamExt;
use ruma::{CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, ServerName};
use super::check_room_id;
use crate::rooms::event_handler::build_local_dag;
impl super::Service {
#[allow(clippy::type_complexity)]
pub(super) async fn fetch_prev<'a, Pdu, Events>(
/// Fetches any missing prev_events for this event and persists them before
/// returning.
pub(super) async fn fetch_prevs(
&self,
origin: &ServerName,
create_event: &Pdu,
room_id: &RoomId,
create_event: &PduEvent,
incoming_pdu: &PduEvent,
origin: &ServerName,
first_ts_in_room: MilliSecondsSinceUnixEpoch,
initial_set: Events,
) -> Result<(
Vec<OwnedEventId>,
HashMap<OwnedEventId, (PduEvent, BTreeMap<String, CanonicalJsonValue>)>,
)>
where
Pdu: Event + Send + Sync,
Events: Iterator<Item = &'a EventId> + Clone + Send,
{
let num_ids = initial_set.clone().count();
let mut eventid_info = HashMap::new();
let mut graph: HashMap<OwnedEventId, _> = HashMap::with_capacity(num_ids);
let mut todo_outlier_stack: VecDeque<OwnedEventId> =
initial_set.map(ToOwned::to_owned).collect();
) -> conduwuit::Result<()> {
let start = Instant::now();
let mut missing = incoming_pdu
.prev_events()
.stream()
.broad_filter_map(|event_id| async move {
self.services
.timeline
.get_non_outlier_pdu_json(event_id)
.await
.is_ok()
.or(|| event_id.to_owned())
})
.collect::<Vec<_>>()
.await;
if missing.is_empty() {
debug!(elapsed=?start.elapsed(), event_id=%incoming_pdu.event_id(), "No missing prev events.");
return Ok(());
}
debug!(elapsed=?start.elapsed(), %room_id, event_id=%incoming_pdu.event_id(), ?missing, "Fetching previous events");
let tail = self
.services
.state
.get_forward_extremities(room_id)
.collect::<Vec<_>>()
.await;
let mut amount = 0;
let mut gapfilled = self
.get_missing_events(
room_id,
incoming_pdu,
tail,
origin,
self.services
.metadata
.get_mindepth(room_id)
.await
.saturating_sub(
u8::try_from(incoming_pdu.prev_events.len())
.unwrap()
.saturating_mul(2)
.into(),
),
)
.await?;
debug_info!(elapsed=?start.elapsed(), "Fetched {} missing events", gapfilled.len());
missing.retain(|eid| !gapfilled.contains_key(eid));
if !missing.is_empty() {
debug_warn!(elapsed=?start.elapsed(), "Still missing {} events, falling back to atomic fetch.", missing.len());
gapfilled.extend(
self.fetch_prev_events(origin, missing, create_event, room_id)
.await,
);
}
while let Some(prev_event_id) = todo_outlier_stack.pop_front() {
self.services.server.check_running()?;
// Persist all fetched events
let mapped = gapfilled
.iter()
.map(|(eid, evt)| {
let mut obj = evt.to_canonical_object();
obj.remove("event_id"); // event_id is inserted by backfill_missing_events
(eid.clone(), obj)
})
.collect::<HashMap<_, _>>();
let to_persist = if mapped.len() <= 1 {
mapped.keys().map(ToOwned::to_owned).collect()
} else {
let refmap: HashMap<OwnedEventId, &CanonicalJsonObject> =
mapped.iter().map(|(id, data)| (id.clone(), data)).collect();
build_local_dag(&refmap).await?
};
let job_start = Instant::now();
trace!("Starting to persist {} prev events", to_persist.len());
for (i, event_id) in to_persist.iter().enumerate() {
debug!(
elapsed=?start.elapsed(),
"Persisting fetched prev event: {event_id} ({}/{})",
i.saturating_add(1),
to_persist.len(),
);
let obj = mapped.get(event_id).cloned().unwrap();
let persist_start = Instant::now();
match self
.fetch_and_handle_outliers(
origin,
once(prev_event_id.as_ref()),
create_event,
room_id,
)
.boxed()
.handle_outlier_pdu(origin, create_event, event_id, room_id, obj)
.await
.pop()
{
| Some((pdu, mut json_opt)) => {
check_room_id(room_id, &pdu)?;
let limit = self.services.server.config.max_fetch_prev_events;
if amount > limit {
debug_warn!("Max prev event limit reached! Limit: {limit}");
graph.insert(prev_event_id.clone(), HashSet::new());
continue;
}
if json_opt.is_none() {
json_opt = self
.services
.outlier
.get_outlier_pdu_json(&prev_event_id)
.await
.ok();
}
if let Some(json) = json_opt {
if pdu.origin_server_ts() > first_ts_in_room {
amount = amount.saturating_add(1);
for prev_prev in pdu.prev_events() {
if !graph.contains_key(prev_prev) {
todo_outlier_stack.push_back(prev_prev.to_owned());
}
}
graph.insert(
prev_event_id.clone(),
pdu.prev_events().map(ToOwned::to_owned).collect(),
);
} else {
// Time based check failed
graph.insert(prev_event_id.clone(), HashSet::new());
}
eventid_info.insert(prev_event_id.clone(), (pdu, json));
} else {
// Get json failed, so this was not fetched over federation
graph.insert(prev_event_id.clone(), HashSet::new());
}
},
| _ => {
// Fetch and handle failed
graph.insert(prev_event_id.clone(), HashSet::new());
| Ok((pdu, val)) if pdu.origin_server_ts() >= first_ts_in_room => {
Box::pin(self.upgrade_outlier_to_timeline_pdu(
pdu,
val,
create_event,
origin,
room_id,
))
.await
.inspect_err(|e| {
debug_warn!(
total_elapsed=?start.elapsed(),
job_elapsed=?job_start.elapsed(),
task_elapsed=?persist_start.elapsed(),
"Failed to upgrade prev event {event_id}: {e}",
);
})
.inspect(|_| {
debug_info!(
total_elapsed=?start.elapsed(),
job_elapsed=?job_start.elapsed(),
task_elapsed=?persist_start.elapsed(),
"Upgraded prev event {event_id}",
);
})
.ok();
},
| Err(e) => debug_warn!(
total_elapsed=?start.elapsed(),
job_elapsed=?job_start.elapsed(),
task_elapsed=?persist_start.elapsed(),
"Failed to persist prev event {event_id}: {e}",
),
| _ => {},
}
}
let event_fetch = |event_id| {
let origin_server_ts = eventid_info
.get(&event_id)
.map_or_else(|| uint!(0), |info| info.0.origin_server_ts().get());
// This return value is the key used for sorting events,
// events are then sorted by power level, time,
// and lexically by event_id.
future::ok((int!(0), MilliSecondsSinceUnixEpoch(origin_server_ts)))
};
let sorted = state_res::lexicographical_topological_sort(&graph, &event_fetch)
.await
.map_err(|e| err!(Database(error!("Error sorting prev events: {e}"))))?;
Ok((sorted, eventid_info))
// NOTE because i keep forgetting: the caller persists incoming_pdu.
// we only care about its prev events
trace!(
total_elapsed=?start.elapsed(),
persist_elapsed=?job_start.elapsed(),
);
Ok(())
}
}
+352 -49
View File
@@ -1,34 +1,43 @@
use std::collections::{HashMap, hash_map};
use conduwuit::{Err, Event, Result, debug, debug_warn, err};
use futures::FutureExt;
use ruma::{
EventId, OwnedEventId, RoomId, ServerName, api::federation::event::get_room_state_ids,
events::StateEventType,
use std::{
cmp::max,
collections::{HashMap, HashSet, hash_map},
hash::{BuildHasherDefault, DefaultHasher},
time::{Duration, Instant},
};
use crate::rooms::short::ShortStateKey;
use conduwuit::{
Err, Event, PduEvent, Result, debug, debug_warn, err, info, trace,
utils::{BoolExt, IterStream},
warn,
};
use futures::{StreamExt, TryFutureExt, future::select_ok};
use ruma::{
EventId, OwnedEventId, OwnedRoomId, RoomId, ServerName,
api::federation::event::{get_room_state, get_room_state_ids},
};
use crate::{conduwuit::utils::stream::BroadbandExt, rooms::short::ShortStateKey};
impl super::Service {
/// Call /state_ids to find out what the state at this pdu is. We trust the
/// server's response to some extent (sic), but we still do a lot of checks
/// on the events
#[tracing::instrument(
level = "debug",
skip_all,
fields(%origin),
)]
pub(super) async fn fetch_state<Pdu>(
/// Asks a remote server what the state at this event is.
/// It first attempts to call `GET /_matrix/federation/v1/state_ids` (fast).
/// If any events are missing, they are fetched from the remote, and
/// persisted as outliers, before being returned back to this function. If
/// we are missing a lot of events locally (>=50), this function falls back
/// to requesting the full state in PDU format from the remote (`GET
/// /_matrix/federation/v1/state, very slow in large rooms), and persists
/// them directly.
#[tracing::instrument(skip_all)]
pub(super) async fn fetch_state(
&self,
origin: &ServerName,
create_event: &Pdu,
create_event: &PduEvent,
room_id: &RoomId,
event_id: &EventId,
) -> Result<Option<HashMap<u64, OwnedEventId>>>
where
Pdu: Event + Send + Sync,
{
let res = self
) -> Result<HashMap<u64, OwnedEventId>> {
let start = Instant::now();
trace!(%origin, "Asking remote for state_ids");
let res: get_room_state_ids::v1::Response = match self
.services
.sending
.send_federation_request(
@@ -36,21 +45,189 @@ pub(super) async fn fetch_state<Pdu>(
get_room_state_ids::v1::Request::new(event_id.to_owned(), room_id.to_owned()),
)
.await
.inspect_err(|e| debug_warn!("Fetching state for event failed: {e}"))?;
.inspect_err(
|e| debug_warn!(elapsed=?start.elapsed(), "Fetching state for event failed: {e}"),
) {
| Ok(resp) => Ok(resp),
| Err(e) =>
if e.is_not_found() {
self.fetch_state_ids_from_backfill_servers(
event_id.to_owned(),
room_id.to_owned(),
)
.await
} else {
Err(e)
},
}?;
debug!("Fetching state events");
let state_ids = res.pdu_ids.iter().map(AsRef::as_ref);
let state_vec = self
.fetch_and_handle_outliers(origin, state_ids, create_event, room_id)
.boxed()
debug!(elapsed=?start.elapsed(), events = res.pdu_ids.len(), "Fetching state events");
let mut state_events: HashMap<OwnedEventId, PduEvent> =
HashMap::with_capacity(res.pdu_ids.len());
let to_fetch: Vec<OwnedEventId> = res
.pdu_ids
.clone()
.into_iter()
.stream()
.broad_filter_map(|event_id| async move {
self.services
.timeline
.pdu_exists(&event_id)
.await
.or_some(event_id)
})
.collect()
.await;
if to_fetch.is_empty() {
debug!(elapsed=?start.elapsed(), "All required state events are already known.");
state_events = res
.pdu_ids
.iter()
.stream()
.broad_filter_map(|event_id| async move {
Some((
event_id.clone(),
self.services
.timeline
.get_pdu(event_id)
.await
.expect("Event disappeared between filtering and fetching"),
))
})
.collect()
.await;
assert_eq!(
state_events.len(),
res.pdu_ids.len(),
"Failed to load all required state events despite allegedly knowing all of them \
already",
);
} else {
let total_count = res.pdu_ids.len();
let missing_count = to_fetch.len();
let missing_threshold = max(50, total_count >> 2);
if missing_count >= missing_threshold {
// If there's more than 50 events to fetch, or we're missing 25% or more of the
// state, we would need to make a lot of atomic requests, so we'll just try
// to fetch the full state from the remote instead.
// Since this endpoint might fail in huge rooms, we fall back to atomic fetch
// anyway.
warn!(
elapsed=?start.elapsed(),
%missing_count,
%total_count,
%missing_threshold,
"Fetching full state from remote server for event"
);
let state_response = tokio::time::timeout(
Duration::from_secs(30),
self.fetch_full_state(origin, create_event, room_id, event_id),
)
.await;
info!(
elapsed=?start.elapsed(),
%missing_count,
%total_count,
%missing_threshold,
"Fetched full state from remote server for event"
);
let fetched_state = match state_response {
| Ok(Ok(state)) => {
// Filter to ensure we only use the PDUs we were expecting, preventing
// arbitrary state injection.
// Atomic fetch does not have this problem as each PDU is evaluated
// individually.
let expected: &HashSet<OwnedEventId, BuildHasherDefault<DefaultHasher>> =
&HashSet::from_iter(res.pdu_ids.clone());
state
.into_iter()
.stream()
.broad_filter_map(|(event_id, pdu)| async move {
expected.contains(&event_id).then_some((event_id, pdu))
})
.collect()
.await
},
| Ok(Err(e)) => {
warn!(
elapsed=?start.elapsed(),
error=?e,
%origin,
"Failed to fetch full state from remote, falling back to atomic fetch"
);
self.fetch_and_handle_auth_events(
origin,
res.pdu_ids.clone(),
create_event,
room_id,
)
.await
},
| Err(e) => {
warn!(
elapsed=?start.elapsed(),
error=?e,
%origin,
"Remote did not return room state in an acceptable timeframe, falling back to atomic fetch"
);
self.fetch_and_handle_auth_events(
origin,
res.pdu_ids.clone(),
create_event,
room_id,
)
.await
},
};
assert!(
!fetched_state.is_empty(),
"fetch_full_state or fetch_and_handle_missing_events returned empty state \
map"
);
state_events.extend(fetched_state);
} else {
state_events = res
.pdu_ids
.iter()
.stream()
.broad_filter_map(|event_id| async move {
self.services
.timeline
.get_pdu(event_id)
.await
.map(|p| (event_id.to_owned(), p))
.ok()
})
.collect()
.await;
assert!(
!state_events.is_empty(),
"Only missing {} events but read-ahead state vec was empty",
to_fetch.len()
);
debug!(
elapsed=?start.elapsed(),
to_fetch = to_fetch.len(),
"Fetching missing events for state from remote"
);
let fetched_state = self
.fetch_and_handle_auth_events(origin, to_fetch, create_event, room_id)
.await;
state_events.extend(fetched_state);
}
}
if state_events.is_empty() {
return Ok(HashMap::new());
}
let mut state: HashMap<ShortStateKey, OwnedEventId> =
HashMap::with_capacity(state_vec.len());
for (pdu, _) in state_vec {
let state_key = pdu
.state_key()
.ok_or_else(|| err!(Database("Found non-state pdu in state events.")))?;
HashMap::with_capacity(state_events.len());
debug!(elapsed=?start.elapsed(), events = state_events.len(), "Processing state events");
for (event_id, pdu) in state_events {
let state_key = pdu.state_key().ok_or_else(|| {
err!(Request(BadJson("Found non-state pdu in state events: {event_id}")))
})?;
let shortstatekey = self
.services
@@ -62,28 +239,154 @@ pub(super) async fn fetch_state<Pdu>(
| hash_map::Entry::Vacant(v) => {
v.insert(pdu.event_id().to_owned());
},
| hash_map::Entry::Occupied(_) => {
return Err!(Database(
"State event's type and state_key combination exists multiple times: \
{}, {}",
| hash_map::Entry::Occupied(existing) => {
return Err!(Request(Forbidden(
"State event's type and state_key combination exists multiple times \
({event_id} + {}): ({}, \"{}\")",
existing.get(),
pdu.kind(),
state_key
));
state_key,
)));
},
}
}
trace!(elapsed=?start.elapsed(), "fetch_state finished");
Ok(state)
}
// The original create event must still be in the state
let create_shortstatekey = self
async fn fetch_state_ids_from_backfill_servers(
&self,
event_id: OwnedEventId,
room_id: OwnedRoomId,
) -> Result<get_room_state_ids::v1::Response> {
let candidates = self
.services
.short
.get_shortstatekey(&StateEventType::RoomCreate, "")
.await?;
if state.get(&create_shortstatekey).map(AsRef::as_ref) != Some(create_event.event_id()) {
return Err!(Database("Incoming event refers to wrong create event."));
.timeline
.candidate_backfill_servers(&room_id)
.await;
if candidates.is_empty() {
return Err!(Request(NotFound(
"Cannot ask any other servers for the state at this event"
)));
}
debug!(%room_id, ?candidates, "Asking backfill servers for state_ids");
let futures = candidates.iter().map(|server_name| {
Box::pin(
self.services
.sending
.send_federation_request(
server_name,
get_room_state_ids::v1::Request::new(event_id.clone(), room_id.clone()),
)
.inspect_err(|e| {
debug_warn!("Fallback fetching state for event failed: {e}");
}),
)
});
Ok(select_ok(futures).await?.0)
}
Ok(Some(state))
/// Fetches the full state via `GET /_matrix/federation/v1/state` from a
/// remote server, and persists all the incoming auth chain events and
/// state events as outliers, for use later.
///
/// Any events that cannot be persisted are dropped with a warning.
pub(super) async fn fetch_full_state(
&self,
origin: &ServerName,
create_event: &PduEvent,
room_id: &RoomId,
event_id: &EventId,
) -> Result<HashMap<OwnedEventId, PduEvent>> {
let start = Instant::now();
trace!("Fetching full state from remote server");
let res: get_room_state::v1::Response = self
.services
.sending
.send_federation_request(
origin,
get_room_state::v1::Request::new(event_id.to_owned(), room_id.to_owned()),
)
.await
.inspect_err(|e| debug_warn!("Fetching state for event failed: {e}"))?;
debug!(elapsed=?start.elapsed(), count = res.auth_chain.len(), "Handling incoming auth chain...");
res.auth_chain
.iter()
.stream()
.broad_filter_map(|raw_event_json| async {
if let Some(parsed) = self.parse_incoming_pdu(raw_event_json).await.ok()
&& parsed.0 == room_id
{
Some(parsed)
} else {
None
}
})
.for_each_concurrent(
None,
|(incoming_room_id, incoming_event_id, incoming_event_json)| async move {
self.handle_outlier_pdu(
origin,
create_event,
&incoming_event_id,
&incoming_room_id,
incoming_event_json,
)
.await
.inspect_err(|e| {
warn!(
%incoming_room_id,
%incoming_event_id,
?e,
"Failed to handle auth chain event from state fetch"
);
})
.ok();
},
)
.await;
debug!(elapsed=?start.elapsed(), count = res.pdus.len(), "Handling incoming state PDUs...");
let r = res
.pdus
.iter()
.stream()
.broad_filter_map(|raw_event_json| async {
if let Some(parsed) = self.parse_incoming_pdu(raw_event_json).await.ok()
&& parsed.0 == room_id
{
Some(parsed)
} else {
None
}
})
.broad_filter_map(
|(incoming_room_id, incoming_event_id, incoming_event_json)| async move {
self.handle_outlier_pdu(
origin,
create_event,
&incoming_event_id,
&incoming_room_id,
incoming_event_json,
)
.await
.inspect_err(|e| {
warn!(
elapsed=?start.elapsed(),
%incoming_room_id,
%incoming_event_id,
?e,
"Failed to handle state event from state fetch"
);
})
.ok()
},
)
.fold(HashMap::new(), |mut acc, (event, _)| async move {
acc.insert(event.event_id().to_owned(), event);
acc
})
.await;
trace!(elapsed=?start.elapsed(), "fetch_full_state finished");
Ok(r)
}
}
@@ -1,14 +1,14 @@
use std::{
collections::{BTreeMap, hash_map},
time::Instant,
collections::BTreeMap,
time::{Duration, Instant},
};
use conduwuit::{
Err, Event, PduEvent, Result, debug::INFO_SPAN_LEVEL, debug_error, debug_info, defer, err,
info, trace, utils::stream::IterStream, warn,
Err, Event, PduEvent, Result, debug, debug_error, debug_info, debug_warn, defer, err, error,
info, matrix::PartialPdu, result::DebugInspect, trace, utils::time::jitter, warn,
};
use futures::{
FutureExt, TryFutureExt, TryStreamExt,
FutureExt, StreamExt,
future::{OptionFuture, try_join4},
};
use ruma::{
@@ -18,7 +18,7 @@
room::member::{MembershipState, RoomMemberEventContent},
},
};
use tracing::debug;
use tokio::sync::mpsc;
use crate::rooms::timeline::{RawPduId, pdu_fits};
@@ -110,10 +110,9 @@ impl super::Service {
/// 14. Check if the event passes auth based on the "current state" of the
/// room, if not soft fail it
#[tracing::instrument(
name = "pdu",
level = INFO_SPAN_LEVEL,
skip_all,
fields(%room_id, %event_id),
name = "pdu",
skip_all,
fields(%room_id, %event_id),
)]
pub async fn handle_incoming_pdu<'a>(
&self,
@@ -153,9 +152,7 @@ pub async fn handle_incoming_pdu<'a>(
.and_then(|v| v.as_str())
.ok_or_else(|| err!("No sender in object"))
.and_then(|v| Ok(UserId::parse(v)?))
.map_err(|e| {
err!(Request(InvalidParam("PDU does not have a valid sender key: {e}")))
})?;
.map_err(|e| err!(Request(BadJson("PDU does not have a valid sender key: {e}"))))?;
let sender_acl_check: OptionFuture<_> = sender
.server_name()
@@ -197,10 +194,14 @@ pub async fn handle_incoming_pdu<'a>(
"Invite to {room_id} appears to have been rescinded by {sender}, \
marking as left"
);
self.services
.state_cache
.mark_as_left(&sender, room_id, Some(pdu))
.await;
self.services.sync.wake(&sender).await;
return Ok(None);
}
}
@@ -235,7 +236,7 @@ pub async fn handle_incoming_pdu<'a>(
}}
let (incoming_pdu, val) = self
.handle_outlier_pdu(origin, create_event, event_id, room_id, value, false)
.handle_outlier_pdu(origin, create_event, event_id, room_id, value)
.await?;
// 8. if not timeline event: stop
@@ -243,71 +244,211 @@ pub async fn handle_incoming_pdu<'a>(
return Ok(None);
}
// Skip old events
// Skip events sent before we joined (they need to be persisted as backfilled
// events, not timeline events, which is handled elsewhere).
let first_ts_in_room = self
.services
.timeline
.first_pdu_in_room(room_id)
.await?
.origin_server_ts();
if incoming_pdu.origin_server_ts() < first_ts_in_room {
return Ok(None);
}
// 9. Fetch any missing prev events doing all checks listed here starting at 1.
// These are timeline events
let (sorted_prev_events, mut eventid_info) = self
.fetch_prev(
origin,
create_event,
room_id,
first_ts_in_room,
incoming_pdu.prev_events(),
)
.await?;
debug!(
events = ?sorted_prev_events,
"Handling previous events"
);
debug!("Fetching and persisting any missing prev events");
Box::pin(self.fetch_prevs(
room_id,
create_event,
&incoming_pdu,
origin,
first_ts_in_room,
))
.await
.debug_inspect_err(|e| {
error!("Failed to fetch and persist incoming event's prev_events: {e:?}");
})?;
sorted_prev_events
.iter()
.try_stream()
.map_ok(AsRef::as_ref)
.try_for_each(|prev_id| {
self.handle_prev_pdu(
origin,
event_id,
room_id,
eventid_info.remove(prev_id),
create_event,
first_ts_in_room,
prev_id,
)
.inspect_err(move |e| {
warn!("Prev {prev_id} failed: {e}");
match self
.services
.globals
.bad_event_ratelimiter
.write()
.entry(prev_id.into())
{
| hash_map::Entry::Vacant(e) => {
e.insert((Instant::now(), 1));
},
| hash_map::Entry::Occupied(mut e) => {
let tries = e.get().1.saturating_add(1);
*e.get_mut() = (Instant::now(), tries);
},
}
})
.map(|_| self.services.server.check_running())
})
.boxed()
.await?;
let is_dummy_event = incoming_pdu.event_type().to_string() == "org.matrix.dummy_event";
// Done with prev events, now handling the incoming event
self.upgrade_outlier_to_timeline_pdu(incoming_pdu, val, create_event, origin, room_id)
.boxed()
.await
let pdu_id = Box::pin(self.upgrade_outlier_to_timeline_pdu(
incoming_pdu,
val,
create_event,
origin,
room_id,
))
.await?;
let extremities_count = self
.services
.state
.get_forward_extremities(room_id)
.count()
.await;
self.maybe_squash_extremities(room_id, extremities_count, is_dummy_event)
.await;
Ok(pdu_id)
}
/// Conditionally starts an extremity squasher. If there is no waiting
/// extremity squasher, a new one is created. Otherwise, the existing one is
/// pinged.
async fn maybe_squash_extremities(
&self,
room_id: &RoomId,
extremities_count: usize,
is_dummy_event: bool,
) {
let (tx, fut) = {
if let Some(tx) = self.extremity_squashers.read().get(room_id)
&& !tx.is_closed()
{
(tx.clone(), None)
} else {
let mut map = self.extremity_squashers.upgradable_read();
if let Some(tx) = map.get(room_id)
&& !tx.is_closed()
{
(tx.clone(), None)
} else {
let (tx, rx) = mpsc::channel(100);
map.with_upgraded(|map| map.insert(room_id.to_owned(), tx.clone()));
(tx, Some(self.spawn_squasher(room_id, rx)))
}
}
};
if let Some(fut) = fut {
fut.await;
}
let _ = tx.try_send((extremities_count, is_dummy_event));
}
/// Spawns an extremity squasher with the given room and receiver channel.
async fn spawn_squasher(&self, room_id: &RoomId, mut rx: mpsc::Receiver<(usize, bool)>) {
let Some(service) = self.me.upgrade() else {
return;
};
let room_id = room_id.to_owned();
self.services.server.runtime().spawn(async move {
let mut latest_extremity_count = None;
let mut non_dummy_event = false;
let mut closing = false;
let waker = tokio::time::sleep(jitter(Duration::from_mins(2), -25.0..=25.0));
tokio::pin!(waker);
loop {
tokio::select! {
msg = rx.recv() => {
if let Some((extremities_count, is_dummy_event)) = msg {
latest_extremity_count = Some(extremities_count);
non_dummy_event = non_dummy_event || !is_dummy_event;
let sleep_duration = if extremities_count >= 20 {
// Skip the original sleep duration and send in the next 3-7 seconds as the number of extremities has grown beyond what one squash can reasonably reduce. We still jitter here in case we receive more events in that time that reduce the number anyway, and to account for other servers sending the same squashes.
jitter(Duration::from_secs(5), -50.0..=50.0)
} else {
jitter(Duration::from_mins(1), -50.0..=50.0)
};
#[allow(clippy::arithmetic_side_effects)]
waker.as_mut().reset(tokio::time::Instant::now() + sleep_duration);
} else {
{let mut map = service.extremity_squashers.write();
if let Some(tx) = map.get(&room_id) && tx.is_closed() {
map.remove(&room_id);
}}
if let Some(count) = latest_extremity_count {
if non_dummy_event && count >= service.services.server.config.dummy_event_threshold.into() {
Self::squash_extremities(&service, &room_id, count).await;
}
}
break;
}
}
() = &mut waker, if !closing => {
if let Some(count) = latest_extremity_count {
if non_dummy_event && count >= service.services.server.config.dummy_event_threshold.into() {
Self::squash_extremities(&service, &room_id, count).await;
}
latest_extremity_count = None;
non_dummy_event = false;
#[allow(clippy::arithmetic_side_effects)]
waker.as_mut().reset(tokio::time::Instant::now() + Duration::from_mins(2));
} else {
rx.close();
closing = true;
}
}
() = service.server_shutdown.notified(), if !closing => {
rx.close();
closing = true;
}
}
}
});
}
/// Squashes extremities in a room by sending dummy events (empty events
/// that are hidden from clients) to the room. It will only send ONE dummy
/// event to squash. If there are more than 20 extremities, multiple calls
/// to `squash_extremities` will be required.
/// Sending the dummy event will be attempted by iterating over each local
/// user currently joined to the room (including deactivated users) until
/// either one of them successfully builds and appends a dummy event PDU, or
/// there are no more users to try.
async fn squash_extremities(&self, room_id: &RoomId, extremities_count: usize) {
debug_warn!(
%extremities_count,
threshold=%self.services.server.config.dummy_event_threshold,
"Attempting to squash extremities after upgrading pdu"
);
// Try to send a dummy event to squash extremities. See issue #1844
let power_levels = self
.services
.state_accessor
.get_room_power_levels(room_id)
.await;
let mut local_users = self.services.state_cache.local_users_in_room(room_id);
while let Some(user_id) = local_users.next().await {
if !power_levels.user_can_send_message(&user_id, "org.matrix.dummy_event".into()) {
trace!(%user_id, "user does not have power level to send dummy event, skipping");
continue;
}
let state_lock = self.services.state.mutex.lock(room_id).await;
if self
.services
.timeline
.build_and_append_pdu(
PartialPdu {
event_type: "org.matrix.dummy_event".into(),
..PartialPdu::default()
},
&user_id,
Some(room_id),
&state_lock,
)
.await
.inspect(|_| debug!(sender=%user_id, "Successfully sent a dummy event"))
.inspect_err(
|e| debug!(sender=%user_id, ?e, "Failed to send a dummy event via user"),
)
.is_ok()
{
return;
}
}
debug_warn!("Unable to squash extremities using any local user");
}
}
@@ -1,11 +1,13 @@
use std::collections::{BTreeMap, HashMap, hash_map};
use conduwuit::{
Err, Event, PduEvent, Result, debug, debug_info, debug_warn, err, state_res, trace, warn,
Err, Event, PduEvent, Result, debug, debug_info, debug_warn, err, info, state_res, trace,
warn,
};
use futures::future::ready;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
api::federation::authorization::get_event_authorization, canonical_json::redact,
events::StateEventType,
};
@@ -16,6 +18,7 @@ impl super::Service {
/// Handles a PDU as an outlier, performing basic checks like signatures and
/// hashes, proclaimed event auth, and then adding it to the outlier tree.
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(name="handle_outlier", skip_all, fields(%event_id))]
pub(super) async fn handle_outlier_pdu<'a, Pdu>(
&self,
origin: &'a ServerName,
@@ -23,7 +26,6 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
event_id: &'a EventId,
room_id: &'a RoomId,
mut value: CanonicalJsonObject,
auth_events_known: bool,
) -> Result<(PduEvent, BTreeMap<String, CanonicalJsonValue>)>
where
Pdu: Event + Send + Sync,
@@ -47,27 +49,38 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
.verify_event(&value, &room_version_rules)
.await
{
| Ok(ruma::signatures::Verified::All) => value,
| Ok(ruma::signatures::Verified::All) => {
if let Ok(pdu_event) = self.services.timeline.get_pdu(event_id).await {
debug!(
"Already have event {event_id} as an outlier or timeline event, not \
re-processing"
);
value.insert(
"event_id".to_owned(),
CanonicalJsonValue::String(event_id.as_str().to_owned()),
);
check_room_id(room_id, &pdu_event)?;
return Ok((pdu_event, value));
}
value
},
| Ok(ruma::signatures::Verified::Signatures) => {
// Redact
debug_info!("Calculated hash does not match (redaction): {event_id}");
let Ok(obj) =
ruma::canonical_json::redact(value, &room_version_rules.redaction, None)
else {
return Err!(Request(InvalidParam("Redaction failed")));
};
// Skip the PDU if it is redacted and we already have it as an outlier event
if self.services.timeline.pdu_exists(event_id).await {
return Err!(Request(InvalidParam(
"Event was redacted and we already knew about it"
)));
if let Ok(pdu_event) = self.services.timeline.get_pdu(event_id).await {
debug!(
"Received a redacted copy of {event_id}, but we already knew about it. \
Re-using known content instead."
);
check_room_id(room_id, &pdu_event)?;
let obj = pdu_event.to_canonical_object();
return Ok((pdu_event, obj));
}
obj
debug_info!("Calculated hash does not match (redaction): {event_id}");
redact(value, &room_version_rules.redaction, None)
.map_err(|e| err!(Request(BadJson("Failed to redact {event_id}: {e}"))))?
},
| Err(e) => {
return Err!(Request(InvalidParam(debug_error!(
return Err!(Request(Forbidden(debug_error!(
"Signature verification failed for {event_id}: {e}"
))));
},
@@ -90,67 +103,81 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
// Fetch all auth events
let mut auth_events: HashMap<OwnedEventId, PduEvent> = HashMap::new();
for aid in pdu_event.auth_events() {
if self.services.pdu_metadata.is_event_rejected(aid).await {
for auth_event_id in pdu_event.auth_events() {
if self
.services
.pdu_metadata
.is_event_rejected(auth_event_id)
.await
{
debug_warn!(
"Rejecting incoming event {} which depends on rejected auth event {aid}",
"Rejecting incoming event {} which depends on rejected auth event \
{auth_event_id}",
event_id,
);
self.services.pdu_metadata.mark_event_rejected(event_id);
return Err!(Request(InvalidParam("Event has rejected auth event: {aid}")));
return Err!(Request(Forbidden(
"Event has rejected auth event: {auth_event_id}"
)));
}
if let Ok(auth_event) = self.services.timeline.get_pdu(aid).await {
if let Ok(auth_event) = self.services.timeline.get_pdu(auth_event_id).await {
check_room_id(room_id, &auth_event)?;
trace!("Found auth event {aid} for outlier event {event_id} locally");
auth_events.insert(aid.to_owned(), auth_event);
trace!("Found auth event {auth_event_id} for outlier event {event_id} locally");
auth_events.insert(auth_event_id.to_owned(), auth_event);
} else {
debug_warn!(
"Could not find auth event {aid} for outlier event {event_id} locally"
"Could not find auth event {auth_event_id} for outlier event {event_id} \
locally"
);
}
}
// Fetch any missing ones & reject invalid ones
let missing_auth_events = if auth_events_known {
pdu_event
.auth_events()
.filter(|id| !auth_events.contains_key(*id))
.collect::<Vec<_>>()
} else {
pdu_event.auth_events().collect::<Vec<_>>()
};
if !missing_auth_events.is_empty() || !auth_events_known {
debug_info!(
"Fetching {} missing auth events for outlier event {event_id}",
missing_auth_events.len()
);
for (pdu, _) in self
.fetch_and_handle_outliers(
if auth_events.len() != pdu_event.auth_events().count() {
info!("Missing some auth events, asking remote for auth chain");
let response: get_event_authorization::v1::Response = self
.services
.sending
.send_federation_request(
origin,
missing_auth_events.iter().copied(),
create_event,
room_id,
get_event_authorization::v1::Request::new(
room_id.to_owned(),
event_id.to_owned(),
),
)
.await
{
auth_events.insert(pdu.event_id().to_owned(), pdu);
.map_err(|e| {
err!(Request(Forbidden(
"Remote server is not divulging incoming event's auth chain: {e}"
)))
})?;
let mut auth_chain_map = HashMap::with_capacity(response.auth_chain.len());
for auth_pdu_json in response.auth_chain {
let (auth_event_room_id, auth_event_id, auth_pdu_json) =
self.parse_incoming_pdu(&auth_pdu_json).await?;
if auth_event_room_id != room_id {
return Err!(Request(Forbidden(
"Auth event {auth_event_id} is in {auth_event_room_id}, not {room_id}."
)));
}
let auth_pdu = PduEvent::from_id_val(&auth_event_id, auth_pdu_json)
.map_err(|e| err!(Request(BadJson("Invalid PDU {auth_event_id}: {e}"))))?;
auth_chain_map.insert(auth_event_id, auth_pdu);
}
for auth_event_id in pdu_event.auth_events() {
if auth_events.contains_key(auth_event_id) {
continue;
}
if let Some(auth_event) = auth_chain_map.get(auth_event_id) {
auth_events.insert(auth_event_id.to_owned(), auth_event.clone());
} else {
return Err!(Request(Forbidden(
"Remote server is not divulging incoming event's auth events (missing: \
{auth_event_id})"
)));
}
}
} else {
debug!("No missing auth events for outlier event {event_id}");
}
// reject if we are still missing some
let still_missing = pdu_event
.auth_events()
.filter(|id| !auth_events.contains_key(*id))
.collect::<Vec<_>>();
if !still_missing.is_empty() {
// Don't reject: this could be a temporary condition
// TODO: use get_missing_events?
return Err!(Request(InvalidParam(
"Could not fetch all auth events for outlier event {event_id}, still missing: \
{still_missing:?}"
)));
}
// 6. Reject "due to auth events" if the event doesn't pass auth based on the
@@ -181,7 +208,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
.outlier
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
self.services.pdu_metadata.mark_event_rejected(event_id);
return Err!(Request(InvalidParam(
return Err!(Request(Forbidden(
"Auth event's type and state_key combination exists multiple times: {}, \
{}",
auth_event.kind,
@@ -191,18 +218,6 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
}
}
// The original create event must be in the auth events
if !matches!(
auth_events_by_key.get(&(StateEventType::RoomCreate, String::new().into())),
Some(_) | None
) {
self.services.pdu_metadata.mark_event_rejected(event_id);
self.services
.outlier
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
return Err!(Request(InvalidParam("Incoming event refers to wrong create event.")));
}
let state_fetch = |ty: &StateEventType, sk: &str| {
let key = (ty.to_owned(), sk.into());
ready(auth_events_by_key.get(&key).map(ToOwned::to_owned))
@@ -1,95 +0,0 @@
use std::{collections::BTreeMap, time::Instant};
use conduwuit::{
Err, Event, PduEvent, Result, debug::INFO_SPAN_LEVEL, defer,
utils::continue_exponential_backoff_secs,
};
use ruma::{CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, RoomId, ServerName};
use tracing::debug;
impl super::Service {
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(
name = "prev",
level = INFO_SPAN_LEVEL,
skip_all,
fields(%prev_id),
)]
pub(super) async fn handle_prev_pdu<'a, Pdu>(
&self,
origin: &'a ServerName,
event_id: &'a EventId,
room_id: &'a RoomId,
eventid_info: Option<(PduEvent, BTreeMap<String, CanonicalJsonValue>)>,
create_event: &'a Pdu,
first_ts_in_room: MilliSecondsSinceUnixEpoch,
prev_id: &'a EventId,
) -> Result
where
Pdu: Event + Send + Sync,
{
// Check for disabled again because it might have changed
if self.services.metadata.is_disabled(room_id).await {
return Err!(Request(Forbidden(debug_warn!(
"Federaton of room {room_id} is currently disabled on this server. Request by \
origin {origin} and event ID {event_id}"
))));
}
if let Some((time, tries)) = self
.services
.globals
.bad_event_ratelimiter
.read()
.get(prev_id)
{
// Exponential backoff
const MIN_DURATION: u64 = 5 * 60;
const MAX_DURATION: u64 = 60 * 60 * 24;
if continue_exponential_backoff_secs(
MIN_DURATION,
MAX_DURATION,
time.elapsed(),
*tries,
) {
debug!(
?tries,
duration = ?time.elapsed(),
"Backing off from prev_event"
);
return Ok(());
}
}
let Some((pdu, json)) = eventid_info else {
return Ok(());
};
// Skip old events
if pdu.origin_server_ts() < first_ts_in_room {
return Ok(());
}
let start_time = Instant::now();
self.federation_handletime
.write()
.insert(room_id.into(), ((*prev_id).to_owned(), start_time));
defer! {{
self.federation_handletime
.write()
.remove(room_id);
}};
self.upgrade_outlier_to_timeline_pdu(pdu, json, create_event, origin, room_id)
.await?;
debug!(
elapsed = ?start_time.elapsed(),
"Handled prev_event",
);
Ok(())
}
}
+12 -5
View File
@@ -4,7 +4,6 @@
mod fetch_state;
mod handle_incoming_pdu;
mod handle_outlier_pdu;
mod handle_prev_pdu;
mod parse_incoming_pdu;
mod policy_server;
mod resolve_state;
@@ -15,19 +14,21 @@
use async_trait::async_trait;
use conduwuit::{Err, Event, PduEvent, Result, Server, SyncRwLock, utils::MutexMap};
pub use fetch_and_handle_outliers::{GET_MISSING_EVENTS_MAX_BATCH_SIZE, build_local_dag};
use ruma::{
OwnedEventId, OwnedRoomId, RoomId, events::room::create::RoomCreateEventContent,
room_version_rules::RoomVersionRules,
};
use tokio::sync::Notify;
use crate::{Dep, globals, rooms, sending, server_keys};
use tokio::sync::{Notify, mpsc};
use crate::{Dep, globals, rooms, sending, server_keys, sync};
pub struct Service {
pub mutex_federation: RoomMutexMap,
pub federation_handletime: SyncRwLock<HandleTimeMap>,
pub extremity_squashers: SyncRwLock<HashMap<OwnedRoomId, mpsc::Sender<(usize, bool)>>>,
services: Services,
server_shutdown: Notify,
me: std::sync::Weak<Self>,
}
struct Services {
@@ -43,6 +44,7 @@ struct Services {
state_cache: Dep<rooms::state_cache::Service>,
state_accessor: Dep<rooms::state_accessor::Service>,
state_compressor: Dep<rooms::state_compressor::Service>,
sync: Dep<sync::Service>,
timeline: Dep<rooms::timeline::Service>,
server: Arc<Server>,
}
@@ -53,9 +55,11 @@ struct Services {
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
Ok(Arc::new_cyclic(|s| Self {
me: s.clone(),
mutex_federation: RoomMutexMap::new(),
federation_handletime: HandleTimeMap::new().into(),
extremity_squashers: SyncRwLock::new(HashMap::new()),
services: Services {
globals: args.depend::<globals::Service>("globals"),
sending: args.depend::<sending::Service>("sending"),
@@ -71,6 +75,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
state_compressor: args
.depend::<rooms::state_compressor::Service>("rooms::state_compressor"),
sync: args.depend::<sync::Service>("sync"),
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
server: args.server.clone(),
},
@@ -91,6 +96,8 @@ async fn memory_usage(&self, out: &mut (dyn Write + Send)) -> Result {
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
fn interrupt(&self) { self.server_shutdown.notify_waiters(); }
async fn clear_cache(&self) {}
}
impl Service {
@@ -7,7 +7,7 @@
use itertools::Itertools;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, RoomId,
RoomVersionId,
RoomVersionId, room_version_rules::RoomVersionRules,
};
use serde_json::value::RawValue as RawJsonValue;
@@ -56,7 +56,10 @@ fn extract_room_id(event_type: &str, pdu: &CanonicalJsonObject) -> Result<OwnedR
/// Parses every entry in an array as an event ID, returning an error if any
/// step fails.
fn expect_event_id_array(value: &CanonicalJsonObject, field: &str) -> Result<Vec<OwnedEventId>> {
pub(super) fn expect_event_id_array(
value: &CanonicalJsonObject,
field: &str,
) -> Result<Vec<OwnedEventId>> {
value
.get(field)
.ok_or_else(|| err!(Request(BadJson("missing field `{field}` on PDU"))))?
@@ -102,6 +105,19 @@ pub fn validate_pdu(&self, pdu: &CanonicalJsonObject) -> Result {
Ok(())
}
pub async fn parse_incoming_pdu_with_known_room(
&self,
pdu: &RawJsonValue,
room_version_rules: &RoomVersionRules,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let (event_id, value) =
gen_event_id_canonical_json(pdu, room_version_rules).map_err(|e| {
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
})?;
self.validate_pdu(&value)?;
Ok((event_id, value))
}
pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
let value = serde_json::from_str::<CanonicalJsonObject>(pdu.get()).map_err(|e| {
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
@@ -5,7 +5,7 @@
};
use conduwuit::{
Result, debug, err, error,
Result, debug, debug_error, err, error,
matrix::{Event, StateMap},
trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, TryWidebandExt},
@@ -37,6 +37,7 @@ pub(super) async fn state_at_incoming_degree_one<Pdu>(
.pdu_shortstatehash(prev_event)
.await
else {
trace!("No shortstatehash for {prev_event}, cannot calculate one-degree state.");
return Ok(None);
};
@@ -100,6 +101,7 @@ pub(super) async fn state_at_incoming_resolved<Pdu>(
.map_ok(move |sstatehash| (sstatehash, prev_event))
})
.try_collect::<HashMap<_, _>>()
.inspect_err(|e| debug_error!("failed to calculate N-degree short state hashes: {e}"))
.await
else {
return Ok(None);
@@ -1,8 +1,9 @@
use std::{borrow::Borrow, sync::Arc, time::Instant};
use conduwuit::{
Err, Result, debug, debug_info, err, info, is_equal_to,
Err, Result, debug, debug_error, debug_info, err, info, is_equal_to,
matrix::{Event, EventTypeExt, PduEvent, StateKey, state_res},
result::DebugInspect,
trace,
utils::{
IterStream,
@@ -23,28 +24,17 @@
};
impl super::Service {
pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
#[tracing::instrument(name="upgrade_outlier", skip_all, fields(event_id=%incoming_pdu.event_id()))]
pub(super) async fn upgrade_outlier_to_timeline_pdu(
&self,
incoming_pdu: PduEvent,
mut val: CanonicalJsonObject,
create_event: &Pdu,
create_event: &PduEvent,
origin: &ServerName,
room_id: &RoomId,
) -> Result<Option<RawPduId>>
where
Pdu: Event + Send + Sync,
{
// Skip the PDU if we already have it as a timeline event
if let Ok(pduid) = self
.services
.timeline
.get_pdu_id(incoming_pdu.event_id())
.await
{
return Ok(Some(pduid));
}
let (rejected, soft_failed) = join!(
) -> Result<Option<RawPduId>> {
let (pduid, rejected, soft_failed) = join!(
self.services.timeline.get_pdu_id(incoming_pdu.event_id()),
self.services
.pdu_metadata
.is_event_rejected(incoming_pdu.event_id()),
@@ -52,17 +42,27 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
.pdu_metadata
.is_event_soft_failed(incoming_pdu.event_id())
);
if rejected {
return Err!(Request(InvalidParam("Event has been rejected")));
if let Ok(id) = pduid {
trace!(event_id=%incoming_pdu.event_id(), "Skipping upgrade of already upgraded PDU");
return Ok(Some(id));
} else if rejected {
return Err!(Request(Forbidden("Event has been rejected")));
} else if soft_failed {
return Err!(Request(InvalidParam("Event has been soft-failed")));
return Err!(Request(Forbidden("Event has been soft-failed")));
}
assert_eq!(
*create_event.kind(),
StateEventType::RoomCreate.into(),
"tried to upgrade a PDU with a create_event that is not a room create event"
);
debug!(
event_id = %incoming_pdu.event_id,
"Upgrading PDU from outlier to timeline"
);
let timer = Instant::now();
let min_depth = self.services.metadata.get_mindepth(room_id).await;
let room_version_rules = get_room_version_rules(create_event)?;
// 10. Fetch missing state and auth chain events by calling /state_ids at
@@ -73,21 +73,34 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
event_id = %incoming_pdu.event_id,
"Resolving state at event"
);
let mut state_at_incoming_event = if incoming_pdu.prev_events().count() == 1 {
let state_at_incoming_event = if incoming_pdu.prev_events().count() == 1 {
self.state_at_incoming_degree_one(&incoming_pdu).await?
} else {
self.state_at_incoming_resolved(&incoming_pdu, room_id, &room_version_rules)
.await?
};
let state_at_incoming_event = match state_at_incoming_event {
| Some(s) => s,
| None => {
trace!("Could not calculate incoming state, asking remote {origin} for it");
self.fetch_state(origin, create_event, room_id, incoming_pdu.event_id())
.await
.debug_inspect_err(|e| {
debug_error!("Could not fetch state from {origin}: {e}");
})?
},
};
if state_at_incoming_event.is_none() {
state_at_incoming_event = self
.fetch_state(origin, create_event, room_id, incoming_pdu.event_id())
.await?;
if state_at_incoming_event.is_empty()
&& *incoming_pdu.event_type() != StateEventType::RoomCreate.into()
{
// This can happen if the remote sends an event but cannot be reached to fetch
// the state at it, and all other servers in the room (which might just be the
// unreachable server) are unable to provide required info.
// returning an error here allows the upgrade to be attempted at another time.
return Err!(Request(Forbidden("Could not resolve incoming state at event")));
}
let state_at_incoming_event =
state_at_incoming_event.expect("we always set this to some above");
trace!(state_events = state_at_incoming_event.len(), "Calculated incoming state");
debug!(
event_id = %incoming_pdu.event_id,
@@ -385,6 +398,12 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
// Event has passed all auth/stateres checks
drop(state_lock);
if incoming_pdu.depth > min_depth && incoming_pdu.state_key().is_some() {
self.services
.metadata
.set_mindepth(room_id, incoming_pdu.depth.into());
trace!("Increased room's min depth from {} to {}", min_depth, incoming_pdu.depth);
}
Ok(pdu_id)
}
+48 -5
View File
@@ -1,7 +1,7 @@
use std::{collections::HashMap, sync::Arc};
use conduwuit::{
Err, Pdu, Result, Server, debug, debug_info, debug_warn, err, error, info, is_true,
Err, Event, Pdu, Result, Server, debug, debug_info, debug_warn, err, error, info, is_true,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
@@ -34,13 +34,13 @@
use crate::{
Dep, antispam, globals,
rooms::{
metadata, outlier, pdu_metadata, short,
event_handler, metadata, outlier, pdu_metadata, short,
state::{self, RoomMutexGuard},
state_accessor, state_cache,
state_compressor::{self, CompressedState, HashSetCompressStateEvent},
timeline::{self, pdu_fits},
},
sending, server_keys, users,
sending, server_keys, sync, users,
};
pub struct Service {
@@ -51,6 +51,7 @@ struct Services {
server: Arc<Server>,
db: Arc<Database>,
antispam: Dep<antispam::Service>,
event_handler: Dep<event_handler::Service>,
globals: Dep<globals::Service>,
metadata: Dep<metadata::Service>,
outlier: Dep<outlier::Service>,
@@ -62,6 +63,7 @@ struct Services {
state_accessor: Dep<state_accessor::Service>,
state_cache: Dep<state_cache::Service>,
state_compressor: Dep<state_compressor::Service>,
sync: Dep<sync::Service>,
timeline: Dep<timeline::Service>,
users: Dep<users::Service>,
}
@@ -73,6 +75,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
server: args.server.clone(),
db: args.db.clone(),
antispam: args.depend::<antispam::Service>("antispam"),
event_handler: args.depend::<event_handler::Service>("rooms::event_handler"),
globals: args.depend::<globals::Service>("globals"),
metadata: args.depend::<metadata::Service>("rooms::metadata"),
outlier: args.depend::<outlier::Service>("rooms::outlier"),
@@ -85,6 +88,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
state_cache: args.depend::<state_cache::Service>("rooms::state_cache"),
state_compressor: args
.depend::<state_compressor::Service>("rooms::state_compressor"),
sync: args.depend::<sync::Service>("sync"),
timeline: args.depend::<timeline::Service>("rooms::timeline"),
users: args.depend::<users::Service>("users"),
},
@@ -380,8 +384,6 @@ pub async fn join_remote_room(
// It has enough fields to be called a proper event now
let mut join_event = join_event_stub;
info!("Asking {remote_server} for send_join in room {room_id}");
let send_join_request = federation::membership::create_join_event::v2::Request::new(
room_id.to_owned(),
event_id.clone(),
@@ -391,6 +393,18 @@ pub async fn join_remote_room(
.await,
);
// NOTE: send_join can take a long time to respond, but from the point of view
// of other servers, we may already have finished joining. This means they
// sometimes end up sending PDUs to us that we aren't yet ready to accept, and
// consequently drop. Holding the mutex over the room while processing mitigates
// this.
let _room_lock = self
.services
.event_handler
.mutex_federation
.lock(room_id.as_str())
.await;
info!("Asking {remote_server} for send_join in room {room_id}");
let send_join_response = match self
.services
.sending
@@ -576,7 +590,13 @@ pub async fn join_remote_room(
if !auth_check {
return Err!(Request(Forbidden("Auth check failed")));
}
let resident_before = self
.services
.state_cache
.server_in_room(self.services.globals.server_name(), room_id)
.await;
let cork = self.services.db.cork_and_flush();
info!("Compressing state from send_join");
let compressed: CompressedState = self
.services
@@ -625,6 +645,10 @@ pub async fn join_remote_room(
room_id,
)
.await?;
self.services
.metadata
.maybe_set_mindepth(room_id, parsed_join_pdu.depth.into())
.await;
info!("Setting final room state for new room");
// We set the room state after inserting the pdu, so that we never have a moment
@@ -632,6 +656,25 @@ pub async fn join_remote_room(
self.services
.state
.set_room_state(room_id, statehash_after_join, &state_lock);
if !resident_before {
// NOTE: We replace local extremities for this room if we were not a resident
// before. We might be doing a remote join to satisfy restricted join rules,
// so we don't want to do this if we're already a resident. Otherwise, we
// want to replace our forward extremities whole-sale in case we were
// desynced.
info!("Replacing local forward extremities");
self.services
.state
.set_forward_extremities(
room_id,
std::iter::once(parsed_join_pdu.event_id()),
&state_lock,
)
.await;
}
drop(cork);
self.services.sync.wake_all_joined(room_id).await;
Ok(())
}
+25 -2
View File
@@ -1,9 +1,9 @@
use std::sync::Arc;
use conduwuit::{Result, utils::stream::TryIgnore};
use database::Map;
use database::{Deserialized, Map};
use futures::{Stream, StreamExt};
use ruma::{OwnedRoomId, RoomId};
use ruma::{OwnedRoomId, RoomId, UInt, uint};
use crate::{Dep, rooms};
@@ -17,6 +17,7 @@ struct Data {
bannedroomids: Arc<Map>,
roomid_shortroomid: Arc<Map>,
pduid_pdu: Arc<Map>,
roomid_mindepth: Arc<Map>,
}
struct Services {
@@ -31,6 +32,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
bannedroomids: args.db["bannedroomids"].clone(),
roomid_shortroomid: args.db["roomid_shortroomid"].clone(),
pduid_pdu: args.db["pduid_pdu"].clone(),
roomid_mindepth: args.db["roomid_mindepth"].clone(),
},
services: Services {
short: args.depend::<rooms::short::Service>("rooms::short"),
@@ -98,4 +100,25 @@ pub async fn is_banned(&self, room_id: &RoomId) -> bool {
pub fn list_banned_rooms(&self) -> impl Stream<Item = OwnedRoomId> + Send + '_ {
self.db.bannedroomids.keys().ignore_err()
}
pub async fn get_mindepth(&self, room_id: &RoomId) -> UInt {
self.db
.roomid_mindepth
.get(room_id)
.await
.deserialized::<UInt>()
.unwrap_or_else(|_| uint!(0))
}
pub fn set_mindepth(&self, room_id: &RoomId, min_depth: u64) {
self.db
.roomid_mindepth
.put_raw(room_id.as_bytes(), min_depth.to_be_bytes());
}
pub async fn maybe_set_mindepth(&self, room_id: &RoomId, min_depth: u64) {
if min_depth > self.get_mindepth(room_id).await.into() {
self.set_mindepth(room_id, min_depth);
}
}
}
+4 -1
View File
@@ -21,7 +21,7 @@
};
use self::data::{Data, ReceiptItem};
use crate::{Dep, rooms, sending};
use crate::{Dep, rooms, sending, sync};
pub struct Service {
services: Services,
@@ -31,6 +31,7 @@ pub struct Service {
struct Services {
sending: Dep<sending::Service>,
short: Dep<rooms::short::Service>,
sync: Dep<sync::Service>,
timeline: Dep<rooms::timeline::Service>,
}
@@ -40,6 +41,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
services: Services {
sending: args.depend::<sending::Service>("sending"),
short: args.depend::<rooms::short::Service>("rooms::short"),
sync: args.depend::<sync::Service>("sync"),
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
},
db: Data::new(&args),
@@ -63,6 +65,7 @@ pub async fn readreceipt_update(
.flush_room(room_id)
.await
.expect("room flush failed");
self.services.sync.wake_all_joined(room_id).await;
}
/// Gets the latest private read receipt from the user in the room

Some files were not shown because too many files have changed in this diff Show More