Compare commits

..

44 Commits

Author SHA1 Message Date
Ginger eba0130f8e fix(complement): Fix complement conflicting with first-run
- Disabled first-run mode when running Complement tests
- Updated logging config under complement to be a bit less verbose
- Changed test result and log output locations
2026-03-03 18:43:53 +00:00
timedout b507898c62 fix: Bump ruwuma again 2026-03-03 18:10:28 +00:00
nexy7574 f4af67575e fix: Bump ruwuma to resolve duplicate state error 2026-03-03 06:01:02 +00:00
timedout 6adb99397e feat: Remove MSC4010 support 2026-02-27 17:03:19 +00:00
Renovate Bot 8ce83a8a14 chore(deps): update rust crate axum-extra to 0.12.0 2026-02-25 17:16:35 +00:00
Niklas Wojtkowiak 052c4dfa21 fix(sync): don't override sliding sync v5 list range start to zero 2026-02-24 13:59:33 +00:00
lynxize a43dee1728 fix: Don't show successful media deletion as an error
Fixes !admin media delete --mxc <url> responding with an error message
when the media was deleted successfully.
2026-02-23 22:02:34 -07:00
Niklas Wojtkowiak 763d9b3de8 fixup! fix(api): restore backwards compatibility for RTC foci config 2026-02-23 18:10:25 -05:00
Niklas Wojtkowiak 1e6d95583c chore(deps): update ruwuma revision 2026-02-23 23:01:15 +00:00
Niklas Wojtkowiak 8a254a33cc fix(api): restore backwards compatibility for RTC foci config 2026-02-23 23:01:15 +00:00
Niklas Wojtkowiak c97dd54766 chore(changelog): add news fragment for #1442 2026-02-23 23:01:15 +00:00
Niklas Wojtkowiak 8ddb7c70c0 feat(api): implement MSC4143 RTC transports discovery endpoint
Add dedicated \`GET /_matrix/client/v1/rtc/transports\` and \`GET /_matrix/client/unstable/org.matrix.msc4143/rtc/transports\` endpoints for MatrixRTC focus discovery (MSC4143), replacing the deprecated well-known approach.

Move RTC foci configuration from \`[global.well_known]\` into a new \`[global.matrix_rtc]\` config section with a \`foci\` field. Remove \`rtc_foci\` from the \`.well-known/matrix/client\` response. Update LiveKit setup documentation accordingly.

Closes #1431
2026-02-23 23:01:15 +00:00
Niklas Wojtkowiak cb9786466b chore(changelog): add news fragment for #1441 2026-02-23 17:59:13 +00:00
Niklas Wojtkowiak 18d2662b01 fix(config): remove allow_public_room_directory_without_auth 2026-02-23 17:59:13 +00:00
timedout 558262dd1f chore: Refactor transaction_ids -> transactions 2026-02-23 17:44:35 +00:00
timedout d311b87579 chore: Fix incorrect capitalisation
I didn't realise I agreed to take an English class with @ginger while
working on this server lol
2026-02-23 17:25:12 +00:00
timedout 8702f55cf5 fix: Don't panic if nobody's listening 2026-02-23 17:22:37 +00:00
timedout d4481b07ac chore: Add news frag 2026-02-23 16:54:54 +00:00
Jade Ellis 92351df925 refactor: Make federation transaction handle errors correctly
We have a dedicated error type that's then matched.
Event sorting is now infallible.
Could probably be cleaned up in a bit.
2026-02-23 16:36:46 +00:00
Jade Ellis 47e2733ea1 refactor: Make stream utils generic over the error type 2026-02-23 16:36:46 +00:00
Jade Ellis 6637e4c6a7 fix: Clean up cache, prevent several race conditions
We use one map which is only ever held for a short time.
2026-02-23 16:36:46 +00:00
nexy7574 35e441452f feat: Attempt to build localised DAG before processing PDUs 2026-02-23 16:36:46 +00:00
nexy7574 66bbb655bf feat: Warn when server is overloaded 2026-02-23 16:36:45 +00:00
nexy7574 81b202ce51 chore: Decrease transaction log verbosity 2026-02-23 16:36:45 +00:00
nexy7574 4657844d46 feat: Show active transaction handle count in !admin federation incoming-federation 2026-02-23 16:36:45 +00:00
nexy7574 9016cd11a6 chore: Run pre-commit and clippy to fix inherited CI errs 2026-02-23 16:36:45 +00:00
nexy7574 dd70094719 feat: Make max_active_txns actually configurable 2026-02-23 16:36:45 +00:00
nexy7574 fcd49b7ab3 fix: Remove duplicate fields from logs 2026-02-23 16:36:45 +00:00
nexy7574 470c9b52dd feat: Instrument process_inbound_transaction 2026-02-23 16:36:45 +00:00
nexy7574 0d8cafc329 feat: Support casting transaction processing to the background 2026-02-23 16:36:44 +00:00
nexy7574 2f9956ddca feat: Add helper functions for federation channels 2026-02-23 16:36:44 +00:00
nexy7574 21a97cdd0b chore: Refactor existing references to transaction service 2026-02-23 16:36:44 +00:00
nexy7574 e986cd4536 feat(federation): Restructure transaction_ids service
Adds two new in-memory maps to the service in to prepare for better handlers
2026-02-23 16:36:40 +00:00
Shane Jaroch 526d862296 fix: more aggressive user agent for URL preview
adding "facebookexternalhit" alongside "embedbot" fixes many errors, such as YouTube Music's:
    "Your browser is deprecated. Please upgrade."

add admin command to clear URL stuck and broken data (per URL currently)

    add command to clear all saved URL previews.
    sync resolver docs.
2026-02-23 15:24:14 +00:00
Ben Botwin fbeb5bf186 report permission denied errors 2026-02-23 15:22:18 +00:00
Ben Botwin a336f2df44 fixed formatting 2026-02-23 15:22:18 +00:00
Ben Botwin 19b78ec73e made error handling more concise 2026-02-23 15:22:18 +00:00
Ben Botwin 27ff2d9363 added more granular error handling for other file fetch function 2026-02-23 15:22:18 +00:00
Ben Botwin 50fa8c3abf ran format 2026-02-23 15:22:18 +00:00
Ben Botwin 18c4be869f added handling for other potential errors 2026-02-23 15:22:18 +00:00
Ben Botwin fc00b96d8b Added proper 404 for not found media and fixed devshell for running tests 2026-02-23 15:22:18 +00:00
Jade Ellis fa4156d8a6 docs: Changelog 2026-02-22 21:19:20 +00:00
Jade Ellis 23638cd714 feat(appservices): MSC3202 Device masquerading for appservices 2026-02-22 21:19:20 +00:00
Raven 9f1a483e76 docs: Add information about partnered homeservers to the introduction page & update README.md
Includes step-by-step directions to ease the lift for those who have ended up
here and who have never created a matrix account or used matrix before in the
past.

Also updates the information in README.md to match, as these should generally be identical.
2026-02-21 18:51:56 -08:00
90 changed files with 1437 additions and 1221 deletions
Generated
+19 -42
View File
@@ -445,13 +445,14 @@ dependencies = [
[[package]] [[package]]
name = "axum-extra" name = "axum-extra"
version = "0.10.3" version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
dependencies = [ dependencies = [
"axum", "axum",
"axum-core", "axum-core",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"headers", "headers",
"http", "http",
@@ -459,8 +460,6 @@ dependencies = [
"http-body-util", "http-body-util",
"mime", "mime",
"pin-project-lite", "pin-project-lite",
"rustversion",
"serde_core",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -1014,7 +1013,6 @@ dependencies = [
"nix", "nix",
"num-traits", "num-traits",
"parking_lot", "parking_lot",
"paste",
"rand 0.10.0", "rand 0.10.0",
"rand_core 0.6.4", "rand_core 0.6.4",
"regex", "regex",
@@ -1028,7 +1026,7 @@ dependencies = [
"serde_regex", "serde_regex",
"smallstr", "smallstr",
"smallvec", "smallvec",
"snafu", "thiserror 2.0.18",
"tikv-jemalloc-ctl", "tikv-jemalloc-ctl",
"tikv-jemalloc-sys", "tikv-jemalloc-sys",
"tikv-jemallocator", "tikv-jemallocator",
@@ -1155,7 +1153,7 @@ dependencies = [
"conduwuit_service", "conduwuit_service",
"futures", "futures",
"rand 0.10.0", "rand 0.10.0",
"snafu", "thiserror 2.0.18",
"tracing", "tracing",
] ]
@@ -1223,7 +1221,7 @@ dependencies = [
[[package]] [[package]]
name = "continuwuity-admin-api" name = "continuwuity-admin-api"
version = "0.1.0" version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"ruma-common", "ruma-common",
"serde", "serde",
@@ -1602,7 +1600,7 @@ dependencies = [
[[package]] [[package]]
name = "draupnir-antispam" name = "draupnir-antispam"
version = "0.1.0" version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"ruma-common", "ruma-common",
"serde", "serde",
@@ -3004,7 +3002,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "meowlnir-antispam" name = "meowlnir-antispam"
version = "0.1.0" version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"ruma-common", "ruma-common",
"serde", "serde",
@@ -4096,7 +4094,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"assign", "assign",
"continuwuity-admin-api", "continuwuity-admin-api",
@@ -4119,7 +4117,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4131,7 +4129,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@@ -4154,7 +4152,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@@ -4186,7 +4184,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap", "indexmap",
@@ -4211,7 +4209,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@@ -4233,7 +4231,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror 2.0.18", "thiserror 2.0.18",
@@ -4242,7 +4240,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4252,7 +4250,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-crate", "proc-macro-crate",
@@ -4267,7 +4265,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4279,7 +4277,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
@@ -4912,27 +4910,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "snafu"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2"
dependencies = [
"snafu-derive",
]
[[package]]
name = "snafu-derive"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.10" version = "0.5.10"
+5 -9
View File
@@ -97,7 +97,7 @@ features = [
] ]
[workspace.dependencies.axum-extra] [workspace.dependencies.axum-extra]
version = "0.10.1" version = "0.12.0"
default-features = false default-features = false
features = ["typed-header", "tracing"] features = ["typed-header", "tracing"]
@@ -307,14 +307,9 @@ features = [
] ]
# Used for conduwuit::Error type # Used for conduwuit::Error type
[workspace.dependencies.snafu] [workspace.dependencies.thiserror]
version = "0.8" version = "2.0.12"
default-features = false default-features = false
features = ["std", "rust_1_81"]
# Used for macro name generation
[workspace.dependencies.paste]
version = "1.0"
# Used when hashing the state # Used when hashing the state
[workspace.dependencies.ring] [workspace.dependencies.ring]
@@ -348,7 +343,7 @@ version = "0.1.2"
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma" git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes" #branch = "conduwuit-changes"
rev = "e087ff15888156942ca2ffe6097d1b4c3fd27628" rev = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
features = [ features = [
"compat", "compat",
"rand", "rand",
@@ -386,6 +381,7 @@ features = [
"unstable-pdu", "unstable-pdu",
"unstable-msc4155", "unstable-msc4155",
"unstable-msc4143", # livekit well_known response "unstable-msc4143", # livekit well_known response
"unstable-msc4284"
] ]
[workspace.dependencies.rust-rocksdb] [workspace.dependencies.rust-rocksdb]
+8 -3
View File
@@ -57,10 +57,15 @@ Continuwuity aims to:
### Can I try it out? ### Can I try it out?
Check out the [documentation](https://continuwuity.org) for installation instructions, or join one of these vetted public homeservers running Continuwuity to get a feel for things! Check out the [documentation](https://continuwuity.org) for installation instructions.
- https://continuwuity.rocks -- A public demo server operated by the Continuwuity Team. If you want to try it out as a user, we have some partnered homeservers you can use:
- https://federated.nexus -- Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. * You can head over to [https://federated.nexus](https://federated.nexus/) in your browser.
* Hit the `Apply to Join` button. Once your request has been accepted, you will receive an email with your username and password.
* Head over to [https://app.federated.nexus](https://app.federated.nexus/) and you can sign in there, or use any other matrix chat client you wish elsewhere.
* Your username for matrix will be in the form of `@username:federated.nexus`, however you can simply use the `username` part to log in. Your password is your password.
* There's also [https://continuwuity.rocks/](https://continuwuity.rocks/). You can register a new account using Cinny via [this convenient link](https://app.cinny.in/register/continuwuity.rocks), or you can use Element or another matrix client *that supports registration*.
### What are we working on? ### What are we working on?
+2 -2
View File
@@ -6,10 +6,10 @@ set -euo pipefail
COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}" COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}"
# A `.jsonl` file to write test logs to # A `.jsonl` file to write test logs to
LOG_FILE="${2:-complement_test_logs.jsonl}" LOG_FILE="${2:-tests/test_results/complement/test_logs.jsonl}"
# A `.jsonl` file to write test results to # A `.jsonl` file to write test results to
RESULTS_FILE="${3:-complement_test_results.jsonl}" RESULTS_FILE="${3:-tests/test_results/complement/test_results.jsonl}"
# The base docker image to use for complement tests # The base docker image to use for complement tests
# You can build the default with `docker build -t continuwuity:complement -f ./docker/complement.Dockerfile .` # You can build the default with `docker build -t continuwuity:complement -f ./docker/complement.Dockerfile .`
+1
View File
@@ -0,0 +1 @@
Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted work. Contributed by @nex and @Jade.
+1
View File
@@ -0,0 +1 @@
Added MSC3202 Device masquerading (not all of MSC3202). This should fix issues with enabling MSC4190 for some Mautrix bridges. Contributed by @Jade
+1
View File
@@ -0,0 +1 @@
Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim.
+1
View File
@@ -0,0 +1 @@
Implement MSC4143 MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new `[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim
+1
View File
@@ -0,0 +1 @@
Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim
+1
View File
@@ -0,0 +1 @@
Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and broken.
+3 -3
View File
@@ -9,10 +9,9 @@ address = "0.0.0.0"
allow_device_name_federation = true allow_device_name_federation = true
allow_guest_registration = true allow_guest_registration = true
allow_public_room_directory_over_federation = true allow_public_room_directory_over_federation = true
allow_public_room_directory_without_auth = true
allow_registration = true allow_registration = true
database_path = "/database" database_path = "/database"
log = "trace,h2=debug,hyper=debug" log = "trace,h2=debug,hyper=debug,conduwuit_database=warn,conduwuit_service::manager=info,conduwuit_api::router=error,conduwuit_router=error,tower_http=error"
port = [8008, 8448] port = [8008, 8448]
trusted_servers = [] trusted_servers = []
only_query_trusted_key_servers = false only_query_trusted_key_servers = false
@@ -25,7 +24,7 @@ url_preview_domain_explicit_denylist = ["*"]
media_compat_file_link = false media_compat_file_link = false
media_startup_check = true media_startup_check = true
prune_missing_media = true prune_missing_media = true
log_colors = true log_colors = false
admin_room_notices = false admin_room_notices = false
allow_check_for_updates = false allow_check_for_updates = false
intentionally_unknown_config_option_for_testing = true intentionally_unknown_config_option_for_testing = true
@@ -48,6 +47,7 @@ federation_idle_timeout = 300
sender_timeout = 300 sender_timeout = 300
sender_idle_timeout = 300 sender_idle_timeout = 300
sender_retry_backoff_limit = 300 sender_retry_backoff_limit = 300
force_disable_first_run_mode = true
[global.tls] [global.tls]
dual_protocol = true dual_protocol = true
+42 -13
View File
@@ -290,6 +290,25 @@
# #
#max_fetch_prev_events = 192 #max_fetch_prev_events = 192
# How many incoming federation transactions the server is willing to be
# processing at any given time before it becomes overloaded and starts
# rejecting further transactions until some slots become available.
#
# Setting this value too low or too high may result in unstable
# federation, and setting it too high may cause runaway resource usage.
#
#max_concurrent_inbound_transactions = 150
# Maximum age (in seconds) for cached federation transaction responses.
# Entries older than this will be removed during cleanup.
#
#transaction_id_cache_max_age_secs = 7200 (2 hours)
# Maximum number of cached federation transaction responses.
# When the cache exceeds this limit, older entries will be removed.
#
#transaction_id_cache_max_entries = 8192
# Default/base connection timeout (seconds). This is used only by URL # Default/base connection timeout (seconds). This is used only by URL
# previews and update/news endpoint checks. # previews and update/news endpoint checks.
# #
@@ -527,12 +546,6 @@
# #
#allow_public_room_directory_over_federation = false #allow_public_room_directory_over_federation = false
# Set this to true to allow your server's public room directory to be
# queried without client authentication (access token) through the Client
# APIs. Set this to false to protect against /publicRooms spiders.
#
#allow_public_room_directory_without_auth = false
# Allow guests/unauthenticated users to access TURN credentials. # Allow guests/unauthenticated users to access TURN credentials.
# #
# This is the equivalent of Synapse's `turn_allow_guests` config option. # This is the equivalent of Synapse's `turn_allow_guests` config option.
@@ -1831,14 +1844,13 @@
# #
#support_mxid = #support_mxid =
# A list of MatrixRTC foci URLs which will be served as part of the # **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
# MSC4143 client endpoint at /.well-known/matrix/client. If you're
# setting up livekit, you'd want something like:
# rtc_focus_server_urls = [
# { type = "livekit", livekit_service_url = "https://livekit.example.com" },
# ]
# #
# To disable, set this to be an empty vector (`[]`). # A list of MatrixRTC foci URLs which will be served as part of the
# MSC4143 client endpoint at /.well-known/matrix/client.
#
# This option is deprecated and will be removed in a future release.
# Please migrate to the new `[global.matrix_rtc]` config section.
# #
#rtc_focus_server_urls = [] #rtc_focus_server_urls = []
@@ -1860,6 +1872,23 @@
# #
#blurhash_max_raw_size = 33554432 #blurhash_max_raw_size = 33554432
[global.matrix_rtc]
# A list of MatrixRTC foci (transports) which will be served via the
# MSC4143 RTC transports endpoint at
# `/_matrix/client/v1/rtc/transports`. If you're setting up livekit,
# you'd want something like:
# ```toml
# [global.matrix_rtc]
# foci = [
# { type = "livekit", livekit_service_url = "https://livekit.example.com" },
# ]
# ```
#
# To disable, set this to an empty list (`[]`).
#
#foci = []
[global.ldap] [global.ldap]
# Whether to enable LDAP login. # Whether to enable LDAP login.
+4 -32
View File
@@ -78,47 +78,19 @@ You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firew
### 3. Telling clients where to find LiveKit ### 3. Telling clients where to find LiveKit
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to your client .well-known file. To do so, in the config section `global.well-known`, add (or modify) the option `rtc_focus_server_urls`. To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to the `[global.matrix_rtc]` config section using the `foci` option.
The variable should be a list of servers serving as MatrixRTC endpoints to serve in the well-known file to the client. The variable should be a list of servers serving as MatrixRTC endpoints. Clients discover these via the `/_matrix/client/v1/rtc/transports` endpoint (MSC4143).
```toml ```toml
rtc_focus_server_urls = [ [global.matrix_rtc]
foci = [
{ type = "livekit", livekit_service_url = "https://livekit.example.com" }, { type = "livekit", livekit_service_url = "https://livekit.example.com" },
] ]
``` ```
Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to. Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.
#### Serving .well-known manually
If you don't let Continuwuity serve your `.well-known` files, you need to add the following lines to your `.well-known/matrix/client` file, remembering to replace the URL with your own `lk-jwt-service` deployment:
```json
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://livekit.example.com"
}
]
```
The final file should look something like this:
```json
{
"m.homeserver": {
"base_url":"https://matrix.example.com"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://livekit.example.com"
}
]
}
```
### 4. Configure your Reverse Proxy ### 4. Configure your Reverse Proxy
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this. Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
+7 -1
View File
@@ -51,7 +51,13 @@ continuwuity aims to:
Check out the [documentation](https://continuwuity.org) for installation instructions. Check out the [documentation](https://continuwuity.org) for installation instructions.
There are currently no open registration continuwuity instances available. If you want to try it out as a user, we have some partnered homeservers you can use:
* You can head over to [https://federated.nexus](https://federated.nexus/) in your browser.
* Hit the `Apply to Join` button. Once your request has been accepted, you will receive an email with your username and password.
* Head over to [https://app.federated.nexus](https://app.federated.nexus/) and you can sign in there, or use any other matrix chat client you wish elsewhere.
* Your username for matrix will be in the form of `@username:federated.nexus`, however you can simply use the `username` part to log in. Your password is your password.
* There's also [https://continuwuity.rocks/](https://continuwuity.rocks/). You can register a new account using Cinny via [this convenient link](https://app.cinny.in/register/continuwuity.rocks), or you can use Element or another matrix client *that supports registration*.
## What are we working on? ## What are we working on?
+4
View File
@@ -36,3 +36,7 @@ Deletes all the local media from a local user on our server. This will always ig
## `!admin media delete-all-from-server` ## `!admin media delete-all-from-server`
Deletes all remote media from the specified remote server. This will always ignore errors by default Deletes all remote media from the specified remote server. This will always ignore errors by default
## `!admin media delete-url-preview`
Deletes a cached URL preview, forcing it to be re-fetched. Use --all to purge all cached URL previews
+5 -2
View File
@@ -30,12 +30,15 @@ pub(super) async fn incoming_federation(&self) -> Result {
.federation_handletime .federation_handletime
.read(); .read();
let mut msg = format!("Handling {} incoming pdus:\n", map.len()); let mut msg = format!(
"Handling {} incoming PDUs across {} active transactions:\n",
map.len(),
self.services.transactions.txn_active_handle_count()
);
for (r, (e, i)) in map.iter() { for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed(); let elapsed = i.elapsed();
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?; writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?;
} }
msg msg
}; };
+19 -1
View File
@@ -29,7 +29,9 @@ pub(super) async fn delete(
.delete(&mxc.as_str().try_into()?) .delete(&mxc.as_str().try_into()?)
.await?; .await?;
return Err!("Deleted the MXC from our database and on our filesystem.",); return self
.write_str("Deleted the MXC from our database and on our filesystem.")
.await;
} }
if let Some(event_id) = event_id { if let Some(event_id) = event_id {
@@ -388,3 +390,19 @@ pub(super) async fn get_remote_thumbnail(
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```")) self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
.await .await
} }
#[admin_command]
pub(super) async fn delete_url_preview(&self, url: Option<String>, all: bool) -> Result {
if all {
self.services.media.clear_url_previews().await;
return self.write_str("Deleted all cached URL previews.").await;
}
let url = url.expect("clap enforces url is required unless --all");
self.services.media.remove_url_preview(&url).await?;
self.write_str(&format!("Deleted cached URL preview for: {url}"))
.await
}
+12
View File
@@ -108,4 +108,16 @@ pub enum MediaCommand {
#[arg(long, default_value("800"))] #[arg(long, default_value("800"))]
height: u32, height: u32,
}, },
/// Deletes a cached URL preview, forcing it to be re-fetched.
/// Use --all to purge all cached URL previews.
DeleteUrlPreview {
/// The URL to clear from the saved preview data
#[arg(required_unless_present = "all")]
url: Option<String>,
/// Purge all cached URL previews
#[arg(long, conflicts_with = "url")]
all: bool,
},
} }
+1 -1
View File
@@ -209,7 +209,7 @@ pub(super) async fn compact(
let parallelism = parallelism.unwrap_or(1); let parallelism = parallelism.unwrap_or(1);
let results = maps let results = maps
.into_iter() .into_iter()
.try_stream() .try_stream::<conduwuit::Error>()
.paralleln_and_then(runtime, parallelism, move |map| { .paralleln_and_then(runtime, parallelism, move |map| {
map.compact_blocking(options.clone())?; map.compact_blocking(options.clone())?;
Ok(map.name().to_owned()) Ok(map.name().to_owned())
+11 -1
View File
@@ -20,7 +20,17 @@ pub enum ResolverCommand {
name: Option<String>, name: Option<String>,
}, },
/// Flush a specific server from the resolver caches or everything /// Flush a given server from the resolver caches or flush them completely
///
/// * Examples:
/// * Flush a specific server:
///
/// `!admin query resolver flush-cache matrix.example.com`
///
/// * Flush all resolver caches completely:
///
/// `!admin query resolver flush-cache --all`
#[command(verbatim_doc_comment)]
FlushCache { FlushCache {
name: Option<OwnedServerName>, name: Option<OwnedServerName>,
+7 -7
View File
@@ -3,7 +3,7 @@ use std::fmt::Write;
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Event, Result, debug_info, err, error, info, Err, Error, Event, Result, debug_info, err, error, info,
matrix::pdu::PduBuilder, matrix::pdu::PduBuilder,
utils::{self, ReadyExt, stream::BroadbandExt}, utils::{self, ReadyExt, stream::BroadbandExt},
warn, warn,
@@ -387,7 +387,7 @@ pub(crate) async fn register_route(
) )
.await?; .await?;
if !worked { if !worked {
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success! // Success!
}, },
@@ -401,7 +401,7 @@ pub(crate) async fn register_route(
&uiaainfo, &uiaainfo,
json, json,
); );
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(Request(NotJson("JSON body is not valid"))); return Err!(Request(NotJson("JSON body is not valid")));
@@ -661,7 +661,7 @@ pub(crate) async fn change_password_route(
.await?; .await?;
if !worked { if !worked {
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success! // Success!
@@ -673,7 +673,7 @@ pub(crate) async fn change_password_route(
.uiaa .uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json); .create(sender_user, body.sender_device(), &uiaainfo, json);
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(Request(NotJson("JSON body is not valid"))); return Err!(Request(NotJson("JSON body is not valid")));
@@ -791,7 +791,7 @@ pub(crate) async fn deactivate_route(
.await?; .await?;
if !worked { if !worked {
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success! // Success!
}, },
@@ -802,7 +802,7 @@ pub(crate) async fn deactivate_route(
.uiaa .uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json); .create(sender_user, body.sender_device(), &uiaainfo, json);
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(Request(NotJson("JSON body is not valid"))); return Err!(Request(NotJson("JSON body is not valid")));
+1 -7
View File
@@ -9,7 +9,7 @@ use ruma::{
}, },
events::{ events::{
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent, AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
GlobalAccountDataEventType, RoomAccountDataEventType, RoomAccountDataEventType,
}, },
serde::Raw, serde::Raw,
}; };
@@ -126,12 +126,6 @@ async fn set_account_data(
))); )));
} }
if event_type_s == GlobalAccountDataEventType::PushRules.to_cow_str() {
return Err!(Request(BadJson(
"This endpoint cannot be used for setting/configuring push rules."
)));
}
let data: serde_json::Value = serde_json::from_str(data.get()) let data: serde_json::Value = serde_json::from_str(data.get())
.map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?; .map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?;
+4 -4
View File
@@ -1,6 +1,6 @@
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, debug, err, utils}; use conduwuit::{Err, Error, Result, debug, err, utils};
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
@@ -232,7 +232,7 @@ pub(crate) async fn delete_devices_route(
.await?; .await?;
if !worked { if !worked {
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success! // Success!
}, },
@@ -243,10 +243,10 @@ pub(crate) async fn delete_devices_route(
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, json); .create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(BadRequest(ErrorKind::NotJson, "Not json.")); return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}, },
}, },
} }
+6 -6
View File
@@ -5,7 +5,7 @@ use std::{
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, debug, debug_warn, err, Err, Error, Result, debug, debug_warn, err,
result::NotFound, result::NotFound,
utils, utils,
utils::{IterStream, stream::WidebandExt}, utils::{IterStream, stream::WidebandExt},
@@ -215,7 +215,7 @@ pub(crate) async fn upload_signing_keys_route(
.await?; .await?;
if !worked { if !worked {
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success! // Success!
}, },
@@ -226,10 +226,10 @@ pub(crate) async fn upload_signing_keys_route(
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, json); .create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(BadRequest(ErrorKind::NotJson, "Not json.")); return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}, },
}, },
} }
@@ -396,12 +396,12 @@ pub(crate) async fn get_key_changes_route(
let from = body let from = body
.from .from
.parse() .parse()
.map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "Invalid `from`.")))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?;
let to = body let to = body
.to .to
.parse() .parse()
.map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "Invalid `to`.")))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?;
device_list_updates.extend( device_list_updates.extend(
services services
+27 -5
View File
@@ -3,9 +3,10 @@ use std::time::Duration;
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, err, error, Err, Result, err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize}, utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
}; };
use conduwuit_core::error;
use conduwuit_service::{ use conduwuit_service::{
Services, Services,
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH}, media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
@@ -69,7 +70,7 @@ pub(crate) async fn create_content_route(
.create(mxc, Some(user), Some(&content_disposition), content_type, &body.file) .create(mxc, Some(user), Some(&content_disposition), content_type, &body.file)
.await .await
{ {
error!("Failed to save uploaded media: {e}"); err!("Failed to save uploaded media: {e}");
return Err!(Request(Unknown("Failed to save uploaded media"))); return Err!(Request(Unknown("Failed to save uploaded media")));
} }
@@ -144,12 +145,22 @@ pub(crate) async fn get_content_route(
server_name: &body.server_name, server_name: &body.server_name,
media_id: &body.media_id, media_id: &body.media_id,
}; };
let FileMeta { let FileMeta {
content, content,
content_type, content_type,
content_disposition, content_disposition,
} = fetch_file(&services, &mxc, user, body.timeout_ms, None).await?; } = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
| std::io::ErrorKind::PermissionDenied => {
error!("Permission denied when trying to read file: {e:?}");
return Err!(Request(Unknown("Unknown error when fetching file.")));
},
| _ => return Err!(Request(Unknown("Unknown error when fetching file."))),
},
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
Ok(get_content::v1::Response { Ok(get_content::v1::Response {
file: content.expect("entire file contents"), file: content.expect("entire file contents"),
@@ -185,7 +196,18 @@ pub(crate) async fn get_content_as_filename_route(
content, content,
content_type, content_type,
content_disposition, content_disposition,
} = fetch_file(&services, &mxc, user, body.timeout_ms, Some(&body.filename)).await?; } = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
| std::io::ErrorKind::PermissionDenied => {
error!("Permission denied when trying to read file: {e:?}");
return Err!(Request(Unknown("Unknown error when fetching file.")));
},
| _ => return Err!(Request(Unknown("Unknown error when fetching file."))),
},
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
Ok(get_content_as_filename::v1::Response { Ok(get_content_as_filename::v1::Response {
file: content.expect("entire file contents"), file: content.expect("entire file contents"),
+3 -3
View File
@@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, at, debug_warn, Err, Error, Result, at, debug_warn,
matrix::{ matrix::{
event::{Event, Matches}, event::{Event, Matches},
pdu::PduCount, pdu::PduCount,
@@ -322,7 +322,7 @@ where
if server_ignored { if server_ignored {
// the sender's server is ignored, so ignore this event // the sender's server is ignored, so ignore this event
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::SenderIgnored { sender: None }, ErrorKind::SenderIgnored { sender: None },
"The sender's server is ignored by this server.", "The sender's server is ignored by this server.",
)); ));
@@ -331,7 +331,7 @@ where
if user_ignored && !services.config.send_messages_from_ignored_users_to_client { if user_ignored && !services.config.send_messages_from_ignored_users_to_client {
// the recipient of this PDU has the sender ignored, and we're not // the recipient of this PDU has the sender ignored, and we're not
// configured to send ignored messages to clients // configured to send ignored messages to clients
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) }, ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) },
"You have ignored this sender.", "You have ignored this sender.",
)); ));
+16 -16
View File
@@ -1,5 +1,5 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, err}; use conduwuit::{Err, Error, Result, err};
use conduwuit_service::Services; use conduwuit_service::Services;
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, CanonicalJsonObject, CanonicalJsonValue,
@@ -243,27 +243,27 @@ pub(crate) async fn set_pushrule_route(
body.before.as_deref(), body.before.as_deref(),
) { ) {
let err = match error { let err = match error {
| InsertPushRuleError::ServerDefaultRuleId => err!(BadRequest( | InsertPushRuleError::ServerDefaultRuleId => Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Rule IDs starting with a dot are reserved for server-default rules.", "Rule IDs starting with a dot are reserved for server-default rules.",
)), ),
| InsertPushRuleError::InvalidRuleId => err!(BadRequest( | InsertPushRuleError::InvalidRuleId => Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Rule ID containing invalid characters.", "Rule ID containing invalid characters.",
)), ),
| InsertPushRuleError::RelativeToServerDefaultRule => err!(BadRequest( | InsertPushRuleError::RelativeToServerDefaultRule => Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Can't place a push rule relatively to a server-default rule.", "Can't place a push rule relatively to a server-default rule.",
)), ),
| InsertPushRuleError::UnknownRuleId => err!(BadRequest( | InsertPushRuleError::UnknownRuleId => Error::BadRequest(
ErrorKind::NotFound, ErrorKind::NotFound,
"The before or after rule could not be found.", "The before or after rule could not be found.",
)), ),
| InsertPushRuleError::BeforeHigherThanAfter => err!(BadRequest( | InsertPushRuleError::BeforeHigherThanAfter => Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"The before rule has a higher priority than the after rule.", "The before rule has a higher priority than the after rule.",
)), ),
| _ => err!(BadRequest(ErrorKind::InvalidParam, "Invalid data.")), | _ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
}; };
return Err(err); return Err(err);
@@ -433,13 +433,13 @@ pub(crate) async fn delete_pushrule_route(
.remove(body.kind.clone(), &body.rule_id) .remove(body.kind.clone(), &body.rule_id)
{ {
let err = match error { let err = match error {
| RemovePushRuleError::ServerDefault => err!(BadRequest( | RemovePushRuleError::ServerDefault => Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Cannot delete a server-default pushrule.", "Cannot delete a server-default pushrule.",
)), ),
| RemovePushRuleError::NotFound => | RemovePushRuleError::NotFound =>
err!(BadRequest(ErrorKind::NotFound, "Push rule not found.")), Error::BadRequest(ErrorKind::NotFound, "Push rule not found."),
| _ => err!(BadRequest(ErrorKind::InvalidParam, "Invalid data.")), | _ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
}; };
return Err(err); return Err(err);
+6 -6
View File
@@ -2,7 +2,7 @@ use std::cmp::max;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Event, Result, RoomVersion, debug, err, info, Err, Error, Event, Result, RoomVersion, debug, err, info,
matrix::{StateKey, pdu::PduBuilder}, matrix::{StateKey, pdu::PduBuilder},
}; };
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
@@ -58,7 +58,7 @@ pub(crate) async fn upgrade_room_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services.server.supported_room_version(&body.new_version) { if !services.server.supported_room_version(&body.new_version) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion, ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.", "This server does not support that room version.",
)); ));
@@ -170,7 +170,7 @@ pub(crate) async fn upgrade_room_route(
"creator".into(), "creator".into(),
json!(&sender_user).try_into().map_err(|e| { json!(&sender_user).try_into().map_err(|e| {
info!("Error forming creation event: {e}"); info!("Error forming creation event: {e}");
err!(BadRequest(ErrorKind::BadJson, "Error forming creation event")) Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
})?, })?,
); );
}, },
@@ -186,13 +186,13 @@ pub(crate) async fn upgrade_room_route(
"room_version".into(), "room_version".into(),
json!(&body.new_version) json!(&body.new_version)
.try_into() .try_into()
.map_err(|_| err!(BadRequest(ErrorKind::BadJson, "Error forming creation event")))?, .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
); );
create_event_content.insert( create_event_content.insert(
"predecessor".into(), "predecessor".into(),
json!(predecessor) json!(predecessor)
.try_into() .try_into()
.map_err(|_| err!(BadRequest(ErrorKind::BadJson, "Error forming creation event")))?, .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
); );
// Validate creation event content // Validate creation event content
@@ -203,7 +203,7 @@ pub(crate) async fn upgrade_room_route(
) )
.is_err() .is_err()
{ {
return Err!(BadRequest(ErrorKind::BadJson, "Error forming creation event")); return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
} }
let create_event_id = services let create_event_id = services
+3 -3
View File
@@ -50,8 +50,8 @@ pub(crate) async fn send_message_event_route(
// Check if this is a new transaction id // Check if this is a new transaction id
if let Ok(response) = services if let Ok(response) = services
.transaction_ids .transactions
.existing_txnid(sender_user, sender_device, &body.txn_id) .get_client_txn(sender_user, sender_device, &body.txn_id)
.await .await
{ {
// The client might have sent a txnid of the /sendToDevice endpoint // The client might have sent a txnid of the /sendToDevice endpoint
@@ -92,7 +92,7 @@ pub(crate) async fn send_message_event_route(
) )
.await?; .await?;
services.transaction_ids.add_txnid( services.transactions.add_client_txnid(
sender_user, sender_user,
sender_device, sender_device,
&body.txn_id, &body.txn_id,
+4 -4
View File
@@ -3,7 +3,7 @@ use std::time::Duration;
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, debug, err, info, Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash}, utils::{self, ReadyExt, hash},
warn, warn,
}; };
@@ -191,7 +191,7 @@ pub(crate) async fn handle_login(
} }
if services.users.is_locked(&user_id).await? { if services.users.is_locked(&user_id).await? {
return Err!(BadRequest(ErrorKind::UserLocked, "This account has been locked.")); return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
} }
if services.users.is_login_disabled(&user_id).await { if services.users.is_login_disabled(&user_id).await {
@@ -390,7 +390,7 @@ pub(crate) async fn login_token_route(
.await?; .await?;
if !worked { if !worked {
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success! // Success!
@@ -402,7 +402,7 @@ pub(crate) async fn login_token_route(
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, json); .create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(Request(NotJson("No JSON body was sent when required."))); return Err!(Request(NotJson("No JSON body was sent when required.")));
+3 -1
View File
@@ -336,7 +336,9 @@ where
let ranges = list.ranges.clone(); let ranges = list.ranges.clone();
for mut range in ranges { for mut range in ranges {
range.0 = uint!(0); range.0 = range
.0
.min(UInt::try_from(active_rooms.len()).unwrap_or(UInt::MAX));
range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1); range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1);
range.1 = range range.1 = range
.1 .1
+6 -6
View File
@@ -1,7 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Result, err}; use conduwuit::{Error, Result};
use conduwuit_service::sending::EduBuf; use conduwuit_service::sending::EduBuf;
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
@@ -26,8 +26,8 @@ pub(crate) async fn send_event_to_device_route(
// Check if this is a new transaction id // Check if this is a new transaction id
if services if services
.transaction_ids .transactions
.existing_txnid(sender_user, sender_device, &body.txn_id) .get_client_txn(sender_user, sender_device, &body.txn_id)
.await .await
.is_ok() .is_ok()
{ {
@@ -66,7 +66,7 @@ pub(crate) async fn send_event_to_device_route(
let event = event let event = event
.deserialize_as() .deserialize_as()
.map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "Event is invalid")))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid"))?;
match target_device_id_maybe { match target_device_id_maybe {
| DeviceIdOrAllDevices::DeviceId(target_device_id) => { | DeviceIdOrAllDevices::DeviceId(target_device_id) => {
@@ -104,8 +104,8 @@ pub(crate) async fn send_event_to_device_route(
// Save transaction id with empty data // Save transaction id with empty data
services services
.transaction_ids .transactions
.add_txnid(sender_user, sender_device, &body.txn_id, &[]); .add_client_txnid(sender_user, sender_device, &body.txn_id, &[]);
Ok(send_event_to_device::v3::Response {}) Ok(send_event_to_device::v3::Response {})
} }
+33 -8
View File
@@ -1,8 +1,11 @@
use axum::{Json, extract::State, response::IntoResponse}; use axum::{Json, extract::State, response::IntoResponse};
use conduwuit::{Err, Result}; use conduwuit::{Error, Result};
use ruma::api::client::discovery::{ use ruma::api::client::{
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo}, discovery::{
discover_support::{self, Contact}, discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_support::{self, Contact},
},
error::ErrorKind,
}; };
use crate::Ruma; use crate::Ruma;
@@ -16,7 +19,7 @@ pub(crate) async fn well_known_client(
) -> Result<discover_homeserver::Response> { ) -> Result<discover_homeserver::Response> {
let client_url = match services.config.well_known.client.as_ref() { let client_url = match services.config.well_known.client.as_ref() {
| Some(url) => url.to_string(), | Some(url) => url.to_string(),
| None => return Err!(BadRequest(ErrorKind::NotFound, "Not found.")), | None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
}; };
Ok(discover_homeserver::Response { Ok(discover_homeserver::Response {
@@ -24,10 +27,32 @@ pub(crate) async fn well_known_client(
identity_server: None, identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }), sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
tile_server: None, tile_server: None,
rtc_foci: services.config.well_known.rtc_focus_server_urls.clone(), rtc_foci: services
.config
.matrix_rtc
.effective_foci(&services.config.well_known.rtc_focus_server_urls)
.to_vec(),
}) })
} }
/// # `GET /_matrix/client/v1/rtc/transports`
/// # `GET /_matrix/client/unstable/org.matrix.msc4143/rtc/transports`
///
/// Returns the list of MatrixRTC foci (transports) configured for this
/// homeserver, implementing MSC4143.
pub(crate) async fn get_rtc_transports(
State(services): State<crate::State>,
_body: Ruma<ruma::api::client::discovery::get_rtc_transports::Request>,
) -> Result<ruma::api::client::discovery::get_rtc_transports::Response> {
Ok(ruma::api::client::discovery::get_rtc_transports::Response::new(
services
.config
.matrix_rtc
.effective_foci(&services.config.well_known.rtc_focus_server_urls)
.to_vec(),
))
}
/// # `GET /.well-known/matrix/support` /// # `GET /.well-known/matrix/support`
/// ///
/// Server support contact and support page of a homeserver's domain. /// Server support contact and support page of a homeserver's domain.
@@ -85,7 +110,7 @@ pub(crate) async fn well_known_support(
if contacts.is_empty() && support_page.is_none() { if contacts.is_empty() && support_page.is_none() {
// No admin room, no configured contacts, and no support page // No admin room, no configured contacts, and no support page
return Err!(BadRequest(ErrorKind::NotFound, "Not found.")); return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
} }
Ok(discover_support::Response { contacts, support_page }) Ok(discover_support::Response { contacts, support_page })
@@ -102,7 +127,7 @@ pub(crate) async fn syncv3_client_server_json(
| Some(url) => url.to_string(), | Some(url) => url.to_string(),
| None => match services.config.well_known.server.as_ref() { | None => match services.config.well_known.server.as_ref() {
| Some(url) => url.to_string(), | Some(url) => url.to_string(),
| None => return Err!(BadRequest(ErrorKind::NotFound, "Not found.")), | None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
}, },
}; };
+1
View File
@@ -184,6 +184,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::put_suspended_status) .ruma_route(&client::put_suspended_status)
.ruma_route(&client::well_known_support) .ruma_route(&client::well_known_support)
.ruma_route(&client::well_known_client) .ruma_route(&client::well_known_client)
.ruma_route(&client::get_rtc_transports)
.route("/_conduwuit/server_version", get(client::conduwuit_server_version)) .route("/_conduwuit/server_version", get(client::conduwuit_server_version))
.route("/_continuwuity/server_version", get(client::conduwuit_server_version)) .route("/_continuwuity/server_version", get(client::conduwuit_server_version))
.ruma_route(&client::room_initial_sync_route) .ruma_route(&client::room_initial_sync_route)
+46 -28
View File
@@ -4,7 +4,7 @@ use axum_extra::{
headers::{Authorization, authorization::Bearer}, headers::{Authorization, authorization::Bearer},
typed_header::TypedHeaderRejectionReason, typed_header::TypedHeaderRejectionReason,
}; };
use conduwuit::{Err, Result, debug_error, err, warn}; use conduwuit::{Err, Error, Result, debug_error, err, warn};
use futures::{ use futures::{
TryFutureExt, TryFutureExt,
future::{ future::{
@@ -14,7 +14,8 @@ use futures::{
pin_mut, pin_mut,
}; };
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId, CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
OwnedUserId, UserId,
api::{ api::{
AuthScheme, IncomingRequest, Metadata, AuthScheme, IncomingRequest, Metadata,
client::{ client::{
@@ -66,23 +67,17 @@ pub(super) async fn auth(
if metadata.authentication == AuthScheme::None { if metadata.authentication == AuthScheme::None {
match metadata { match metadata {
| &get_public_rooms::v3::Request::METADATA => { | &get_public_rooms::v3::Request::METADATA => {
if !services match token {
.server | Token::Appservice(_) | Token::User(_) => {
.config // we should have validated the token above
.allow_public_room_directory_without_auth // already
{ },
match token { | Token::None | Token::Invalid => {
| Token::Appservice(_) | Token::User(_) => { return Err(Error::BadRequest(
// we should have validated the token above ErrorKind::MissingToken,
// already "Missing or invalid access token.",
}, ));
| Token::None | Token::Invalid => { },
return Err!(BadRequest(
ErrorKind::MissingToken,
"Missing or invalid access token.",
));
},
}
} }
}, },
| &get_profile::v3::Request::METADATA | &get_profile::v3::Request::METADATA
@@ -96,7 +91,7 @@ pub(super) async fn auth(
// already // already
}, },
| Token::None | Token::Invalid => { | Token::None | Token::Invalid => {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::MissingToken, ErrorKind::MissingToken,
"Missing or invalid access token.", "Missing or invalid access token.",
)); ));
@@ -130,10 +125,10 @@ pub(super) async fn auth(
appservice_info: None, appservice_info: None,
}) })
} else { } else {
Err!(BadRequest(ErrorKind::MissingToken, "Missing access token.")) Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))
} }
}, },
| _ => Err!(BadRequest(ErrorKind::MissingToken, "Missing access token.")), | _ => Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")),
}, },
| ( | (
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None, AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
@@ -149,7 +144,7 @@ pub(super) async fn auth(
&ruma::api::client::session::logout::v3::Request::METADATA &ruma::api::client::session::logout::v3::Request::METADATA
| &ruma::api::client::session::logout_all::v3::Request::METADATA | &ruma::api::client::session::logout_all::v3::Request::METADATA
) { ) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::UserLocked, ErrorKind::UserLocked,
"This account has been locked.", "This account has been locked.",
)); ));
@@ -174,11 +169,11 @@ pub(super) async fn auth(
appservice_info: None, appservice_info: None,
}), }),
| (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => | (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) =>
Err!(BadRequest( Err(Error::BadRequest(
ErrorKind::Unauthorized, ErrorKind::Unauthorized,
"Only server signatures should be used on this endpoint.", "Only server signatures should be used on this endpoint.",
)), )),
| (AuthScheme::AppserviceToken, Token::User(_)) => Err!(BadRequest( | (AuthScheme::AppserviceToken, Token::User(_)) => Err(Error::BadRequest(
ErrorKind::Unauthorized, ErrorKind::Unauthorized,
"Only appservice access tokens should be used on this endpoint.", "Only appservice access tokens should be used on this endpoint.",
)), )),
@@ -196,13 +191,13 @@ pub(super) async fn auth(
appservice_info: None, appservice_info: None,
}) })
} else { } else {
Err!(BadRequest( Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false }, ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.", "Unknown access token.",
)) ))
} }
}, },
| (_, Token::Invalid) => Err!(BadRequest( | (_, Token::Invalid) => Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false }, ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.", "Unknown access token.",
)), )),
@@ -234,10 +229,33 @@ async fn auth_appservice(
return Err!(Request(Exclusive("User is not in namespace."))); return Err!(Request(Exclusive("User is not in namespace.")));
} }
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
// The device_id can be provided via `device_id` or
// `org.matrix.msc3202.device_id` query parameter.
let sender_device = if let Some(ref device_id_str) = request.query.device_id {
let device_id: &DeviceId = device_id_str.as_str().into();
// Verify the device exists for this user
if services
.users
.get_device_metadata(&user_id, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as this device."
)));
}
Some(device_id.to_owned())
} else {
None
};
Ok(Auth { Ok(Auth {
origin: None, origin: None,
sender_user: Some(user_id), sender_user: Some(user_id),
sender_device: None, sender_device,
appservice_info: Some(*info), appservice_info: Some(*info),
}) })
} }
+4
View File
@@ -11,6 +11,10 @@ use service::Services;
pub(super) struct QueryParams { pub(super) struct QueryParams {
pub(super) access_token: Option<String>, pub(super) access_token: Option<String>,
pub(super) user_id: Option<String>, pub(super) user_id: Option<String>,
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
#[serde(alias = "org.matrix.msc3202.device_id")]
pub(super) device_id: Option<String>,
} }
pub(super) struct Request { pub(super) struct Request {
+6 -3
View File
@@ -1,9 +1,12 @@
use std::{borrow::Borrow, iter::once}; use std::{borrow::Borrow, iter::once};
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Error, Result, err, info, utils::stream::ReadyExt}; use conduwuit::{Err, Error, Result, info, utils::stream::ReadyExt};
use futures::StreamExt; use futures::StreamExt;
use ruma::{RoomId, api::federation::authorization::get_event_authorization}; use ruma::{
RoomId,
api::{client::error::ErrorKind, federation::authorization::get_event_authorization},
};
use super::AccessCheck; use super::AccessCheck;
use crate::Ruma; use crate::Ruma;
@@ -44,7 +47,7 @@ pub(crate) async fn get_event_authorization_route(
.timeline .timeline
.get_pdu_json(&body.event_id) .get_pdu_json(&body.event_id)
.await .await
.map_err(|_| err!(BadRequest(ErrorKind::NotFound, "Event not found.")))?; .map_err(|_| Error::BadRequest(ErrorKind::NotFound, "Event not found."))?;
let room_id_str = event let room_id_str = event
.get("room_id") .get("room_id")
+2 -2
View File
@@ -2,7 +2,7 @@ use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use conduwuit::{ use conduwuit::{
Err, PduEvent, Result, err, error, Err, Error, PduEvent, Result, err, error,
matrix::{Event, event::gen_event_id}, matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256}, utils::{self, hash::sha256},
warn, warn,
@@ -33,7 +33,7 @@ pub(crate) async fn create_invite_route(
.await?; .await?;
if !services.server.supported_room_version(&body.room_version) { if !services.server.supported_room_version(&body.room_version) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::IncompatibleRoomVersion { room_version: body.room_version.clone() }, ErrorKind::IncompatibleRoomVersion { room_version: body.room_version.clone() },
"Server does not support this room version.", "Server does not support this room version.",
)); ));
+2 -2
View File
@@ -1,7 +1,7 @@
use std::borrow::ToOwned; use std::borrow::ToOwned;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn}; use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
@@ -80,7 +80,7 @@ pub(crate) async fn create_join_event_template_route(
let room_version_id = services.rooms.state.get_room_version(&body.room_id).await?; let room_version_id = services.rooms.state.get_room_version(&body.room_id).await?;
if !body.ver.contains(&room_version_id) { if !body.ver.contains(&room_version_id) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::IncompatibleRoomVersion { room_version: room_version_id }, ErrorKind::IncompatibleRoomVersion { room_version: room_version_id },
"Room version not supported.", "Room version not supported.",
)); ));
+3 -3
View File
@@ -1,6 +1,6 @@
use RoomVersionId::*; use RoomVersionId::*;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, debug_warn, info, matrix::pdu::PduBuilder, warn}; use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, warn};
use ruma::{ use ruma::{
RoomVersionId, RoomVersionId,
api::{client::error::ErrorKind, federation::knock::create_knock_event_template}, api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
@@ -67,14 +67,14 @@ pub(crate) async fn create_knock_event_template_route(
let room_version_id = services.rooms.state.get_room_version(&body.room_id).await?; let room_version_id = services.rooms.state.get_room_version(&body.room_id).await?;
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6) { if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::IncompatibleRoomVersion { room_version: room_version_id }, ErrorKind::IncompatibleRoomVersion { room_version: room_version_id },
"Room version does not support knocking.", "Room version does not support knocking.",
)); ));
} }
if !body.ver.contains(&room_version_id) { if !body.ver.contains(&room_version_id) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::IncompatibleRoomVersion { room_version: room_version_id }, ErrorKind::IncompatibleRoomVersion { room_version: room_version_id },
"Your homeserver does not support the features required to knock on this room.", "Your homeserver does not support the features required to knock on this room.",
)); ));
+5 -11
View File
@@ -1,6 +1,6 @@
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, err}; use conduwuit::{Error, Result};
use ruma::{ use ruma::{
api::{ api::{
client::error::ErrorKind, client::error::ErrorKind,
@@ -25,7 +25,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
.config .config
.allow_public_room_directory_over_federation .allow_public_room_directory_over_federation
{ {
return Err!(BadRequest(ErrorKind::forbidden(), "Room directory is not public")); return Err(Error::BadRequest(ErrorKind::forbidden(), "Room directory is not public"));
} }
let response = crate::client::get_public_rooms_filtered_helper( let response = crate::client::get_public_rooms_filtered_helper(
@@ -38,10 +38,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
) )
.await .await
.map_err(|_| { .map_err(|_| {
err!(BadRequest( Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
ErrorKind::Unknown,
"Failed to return this server's public room list."
))
})?; })?;
Ok(get_public_rooms_filtered::v1::Response { Ok(get_public_rooms_filtered::v1::Response {
@@ -65,7 +62,7 @@ pub(crate) async fn get_public_rooms_route(
.globals .globals
.allow_public_room_directory_over_federation() .allow_public_room_directory_over_federation()
{ {
return Err!(BadRequest(ErrorKind::forbidden(), "Room directory is not public")); return Err(Error::BadRequest(ErrorKind::forbidden(), "Room directory is not public"));
} }
let response = crate::client::get_public_rooms_filtered_helper( let response = crate::client::get_public_rooms_filtered_helper(
@@ -78,10 +75,7 @@ pub(crate) async fn get_public_rooms_route(
) )
.await .await
.map_err(|_| { .map_err(|_| {
err!(BadRequest( Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
ErrorKind::Unknown,
"Failed to return this server's public room list."
))
})?; })?;
Ok(get_public_rooms::v1::Response { Ok(get_public_rooms::v1::Response {
+6 -5
View File
@@ -1,7 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, err}; use conduwuit::{Error, Result, err};
use futures::StreamExt; use futures::StreamExt;
use get_profile_information::v1::ProfileField; use get_profile_information::v1::ProfileField;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
@@ -67,16 +67,17 @@ pub(crate) async fn get_profile_information_route(
.config .config
.allow_inbound_profile_lookup_federation_requests .allow_inbound_profile_lookup_federation_requests
{ {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::forbidden(), ErrorKind::forbidden(),
"Profile lookup over federation is not allowed on this homeserver.", "Profile lookup over federation is not allowed on this homeserver.",
)); ));
} }
if !services.globals.server_is_ours(body.user_id.server_name()) { if !services.globals.server_is_ours(body.user_id.server_name()) {
return Err!( return Err(Error::BadRequest(
BadRequest(ErrorKind::InvalidParam, "User does not belong to this server.",) ErrorKind::InvalidParam,
); "User does not belong to this server.",
));
} }
let mut displayname = None; let mut displayname = None;
+214 -64
View File
@@ -1,27 +1,33 @@
use std::{collections::BTreeMap, net::IpAddr, time::Instant}; use std::{
collections::{BTreeMap, HashMap, HashSet},
net::IpAddr,
time::{Duration, Instant},
};
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Error, Result, debug, debug_warn, err, error, Err, Error, Result, debug, debug_warn, err, error,
result::LogErr, result::LogErr,
state_res::lexicographical_topological_sort,
trace, trace,
utils::{ utils::{
IterStream, ReadyExt, millis_since_unix_epoch, IterStream, ReadyExt, millis_since_unix_epoch,
stream::{BroadbandExt, TryBroadbandExt, automatic_width}, stream::{BroadbandExt, TryBroadbandExt, automatic_width},
}, },
warn,
}; };
use conduwuit_service::{ use conduwuit_service::{
Services, Services,
sending::{EDU_LIMIT, PDU_LIMIT}, sending::{EDU_LIMIT, PDU_LIMIT},
}; };
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt}; use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
use http::StatusCode;
use itertools::Itertools; use itertools::Itertools;
use ruma::{ use ruma::{
CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId, CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId,
RoomId, ServerName, UserId,
api::{ api::{
client::error::ErrorKind, client::error::{ErrorKind, ErrorKind::LimitExceeded},
federation::transactions::{ federation::transactions::{
edu::{ edu::{
DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent, DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
@@ -32,9 +38,16 @@ use ruma::{
}, },
}, },
events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType}, events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
int,
serde::Raw, serde::Raw,
to_device::DeviceIdOrAllDevices, to_device::DeviceIdOrAllDevices,
uint,
}; };
use service::transactions::{
FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse,
};
use tokio::sync::watch::{Receiver, Sender};
use tracing::instrument;
use crate::Ruma; use crate::Ruma;
@@ -44,15 +57,6 @@ type Pdu = (OwnedRoomId, OwnedEventId, CanonicalJsonObject);
/// # `PUT /_matrix/federation/v1/send/{txnId}` /// # `PUT /_matrix/federation/v1/send/{txnId}`
/// ///
/// Push EDUs and PDUs to this server. /// Push EDUs and PDUs to this server.
#[tracing::instrument(
name = "txn",
level = "debug",
skip_all,
fields(
%client,
origin = body.origin().as_str()
),
)]
pub(crate) async fn send_transaction_message_route( pub(crate) async fn send_transaction_message_route(
State(services): State<crate::State>, State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
@@ -76,16 +80,73 @@ pub(crate) async fn send_transaction_message_route(
))); )));
} }
let txn_start_time = Instant::now(); let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
trace!(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = %body.transaction_id,
origin = %body.origin(),
"Starting txn",
);
// Atomically check cache, join active, or start new transaction
match services
.transactions
.get_or_start_federation_txn(txn_key.clone())?
{
| FederationTxnState::Cached(response) => {
// Already responded
Ok(response)
},
| FederationTxnState::Active(receiver) => {
// Another thread is processing
wait_for_result(receiver).await
},
| FederationTxnState::Started { receiver, sender } => {
// We're the first, spawn the processing task
services
.server
.runtime()
.spawn(process_inbound_transaction(services, body, client, txn_key, sender));
// and wait for it
wait_for_result(receiver).await
},
}
}
async fn wait_for_result(
mut recv: Receiver<WrappedTransactionResponse>,
) -> Result<send_transaction_message::v1::Response> {
if tokio::time::timeout(Duration::from_secs(50), recv.changed())
.await
.is_err()
{
// Took too long, return 429 to encourage the sender to try again
return Err(Error::BadRequest(
LimitExceeded { retry_after: None },
"Transaction is being still being processed. Please try again later.",
));
}
let value = recv.borrow_and_update();
match value.clone() {
| Some(Ok(response)) => Ok(response),
| Some(Err(err)) => Err(transaction_error_to_response(&err)),
| None => Err(Error::Request(
ErrorKind::Unknown,
"Transaction processing failed unexpectedly".into(),
StatusCode::INTERNAL_SERVER_ERROR,
)),
}
}
#[instrument(
skip_all,
fields(
id = ?body.transaction_id.as_str(),
origin = ?body.origin()
)
)]
async fn process_inbound_transaction(
services: crate::State,
body: Ruma<send_transaction_message::v1::Request>,
client: IpAddr,
txn_key: TxnKey,
sender: Sender<WrappedTransactionResponse>,
) {
let txn_start_time = Instant::now();
let pdus = body let pdus = body
.pdus .pdus
.iter() .iter()
@@ -102,40 +163,79 @@ pub(crate) async fn send_transaction_message_route(
.filter_map(Result::ok) .filter_map(Result::ok)
.stream(); .stream();
let results = handle(&services, &client, body.origin(), txn_start_time, pdus, edus).await?; debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",);
let results = match handle(&services, &client, body.origin(), pdus, edus).await {
| Ok(results) => results,
| Err(err) => {
fail_federation_txn(services, &txn_key, &sender, err);
return;
},
};
for (id, result) in &results {
if let Err(e) = result {
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
debug_warn!("Incoming PDU failed {id}: {e:?}");
}
}
}
debug!( debug!(
pdus = body.pdus.len(), pdus = body.pdus.len(),
edus = body.edus.len(), edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(), elapsed = ?txn_start_time.elapsed(),
id = %body.transaction_id, "Finished processing transaction"
origin = %body.origin(),
"Finished txn",
); );
for (id, result) in &results {
if let Err(e) = result {
if matches!(e, Error::BadRequest { kind: ErrorKind::NotFound, .. }) {
warn!("Incoming PDU failed {id}: {e:?}");
}
}
}
Ok(send_transaction_message::v1::Response { let response = send_transaction_message::v1::Response {
pdus: results pdus: results
.into_iter() .into_iter()
.map(|(e, r)| (e, r.map_err(error::sanitized_message))) .map(|(e, r)| (e, r.map_err(error::sanitized_message)))
.collect(), .collect(),
}) };
services
.transactions
.finish_federation_txn(txn_key, sender, response);
} }
/// Handles a failed federation transaction by sending the error through
/// the channel and cleaning up the transaction state. This allows waiters to
/// receive an appropriate error response.
fn fail_federation_txn(
services: crate::State,
txn_key: &TxnKey,
sender: &Sender<WrappedTransactionResponse>,
err: TransactionError,
) {
debug!("Transaction failed: {err}");
// Remove from active state so the transaction can be retried
services.transactions.remove_federation_txn(txn_key);
// Send the error to any waiters
if let Err(e) = sender.send(Some(Err(err))) {
debug_warn!("Failed to send transaction error to receivers: {e}");
}
}
/// Converts a TransactionError into an appropriate HTTP error response.
fn transaction_error_to_response(err: &TransactionError) -> Error {
match err {
| TransactionError::ShuttingDown => Error::Request(
ErrorKind::Unknown,
"Server is shutting down, please retry later".into(),
StatusCode::SERVICE_UNAVAILABLE,
),
}
}
async fn handle( async fn handle(
services: &Services, services: &Services,
client: &IpAddr, client: &IpAddr,
origin: &ServerName, origin: &ServerName,
started: Instant,
pdus: impl Stream<Item = Pdu> + Send, pdus: impl Stream<Item = Pdu> + Send,
edus: impl Stream<Item = Edu> + Send, edus: impl Stream<Item = Edu> + Send,
) -> Result<ResolvedMap> { ) -> std::result::Result<ResolvedMap, TransactionError> {
// group pdus by room // group pdus by room
let pdus = pdus let pdus = pdus
.collect() .collect()
@@ -152,7 +252,7 @@ async fn handle(
.into_iter() .into_iter()
.try_stream() .try_stream()
.broad_and_then(|(room_id, pdus): (_, Vec<_>)| { .broad_and_then(|(room_id, pdus): (_, Vec<_>)| {
handle_room(services, client, origin, started, room_id, pdus.into_iter()) handle_room(services, client, origin, room_id, pdus.into_iter())
.map_ok(Vec::into_iter) .map_ok(Vec::into_iter)
.map_ok(IterStream::try_stream) .map_ok(IterStream::try_stream)
}) })
@@ -169,14 +269,51 @@ async fn handle(
Ok(results) Ok(results)
} }
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
/// returning them in a topologically sorted order.
///
/// This is used to attempt to process PDUs in an order that respects their
/// dependencies, however it is ultimately the sender's responsibility to send
/// them in a processable order, so this is just a best effort attempt. It does
/// not account for power levels or other tie breaks.
async fn build_local_dag(
pdu_map: &HashMap<OwnedEventId, CanonicalJsonObject>,
) -> Result<Vec<OwnedEventId>> {
debug_assert!(pdu_map.len() >= 2, "needless call to build_local_dag with less than 2 PDUs");
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> = HashMap::new();
for (event_id, value) in pdu_map {
let prev_events = value
.get("prev_events")
.expect("pdu must have prev_events")
.as_array()
.expect("prev_events must be an array")
.iter()
.map(|v| {
OwnedEventId::parse(v.as_str().expect("prev_events values must be strings"))
.expect("prev_events must be valid event IDs")
})
.collect::<HashSet<OwnedEventId>>();
dag.insert(event_id.clone(), prev_events);
}
lexicographical_topological_sort(&dag, &|_| async {
// Note: we don't bother fetching power levels because that would massively slow
// this function down. This is a best-effort attempt to order events correctly
// for processing, however ultimately that should be the sender's job.
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.map_err(|e| err!("failed to resolve local graph: {e}"))
}
async fn handle_room( async fn handle_room(
services: &Services, services: &Services,
_client: &IpAddr, _client: &IpAddr,
origin: &ServerName, origin: &ServerName,
txn_start_time: Instant,
room_id: OwnedRoomId, room_id: OwnedRoomId,
pdus: impl Iterator<Item = Pdu> + Send, pdus: impl Iterator<Item = Pdu> + Send,
) -> Result<Vec<(OwnedEventId, Result)>> { ) -> std::result::Result<Vec<(OwnedEventId, Result)>, TransactionError> {
let _room_lock = services let _room_lock = services
.rooms .rooms
.event_handler .event_handler
@@ -185,27 +322,40 @@ async fn handle_room(
.await; .await;
let room_id = &room_id; let room_id = &room_id;
pdus.try_stream() let pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus
.and_then(|(_, event_id, value)| async move { .into_iter()
services.server.check_running()?; .map(|(_, event_id, value)| (event_id, value))
let pdu_start_time = Instant::now(); .collect();
let result = services // Try to sort PDUs by their dependencies, but fall back to arbitrary order on
.rooms // failure (e.g., cycles). This is best-effort; proper ordering is the sender's
.event_handler // responsibility.
.handle_incoming_pdu(origin, room_id, &event_id, value, true) let sorted_event_ids = if pdu_map.len() >= 2 {
.await build_local_dag(&pdu_map).await.unwrap_or_else(|e| {
.map(|_| ()); debug_warn!("Failed to build local DAG for room {room_id}: {e}");
pdu_map.keys().cloned().collect()
debug!(
pdu_elapsed = ?pdu_start_time.elapsed(),
txn_elapsed = ?txn_start_time.elapsed(),
"Finished PDU {event_id}",
);
Ok((event_id, result))
}) })
.try_collect() } else {
.await pdu_map.keys().cloned().collect()
};
let mut results = Vec::with_capacity(sorted_event_ids.len());
for event_id in sorted_event_ids {
let value = pdu_map
.get(&event_id)
.expect("sorted event IDs must be from the original map")
.clone();
services
.server
.check_running()
.map_err(|_| TransactionError::ShuttingDown)?;
let result = services
.rooms
.event_handler
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
.await
.map(|_| ());
results.push((event_id, result));
}
Ok(results)
} }
async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) { async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) {
@@ -478,8 +628,8 @@ async fn handle_edu_direct_to_device(
// Check if this is a new transaction id // Check if this is a new transaction id
if services if services
.transaction_ids .transactions
.existing_txnid(sender, None, message_id) .get_client_txn(sender, None, message_id)
.await .await
.is_ok() .is_ok()
{ {
@@ -498,8 +648,8 @@ async fn handle_edu_direct_to_device(
// Save transaction id with empty data // Save transaction id with empty data
services services
.transaction_ids .transactions
.add_txnid(sender, None, message_id, &[]); .add_client_txnid(sender, None, message_id, &[]);
} }
async fn handle_edu_direct_to_device_user<Event: Send + Sync>( async fn handle_edu_direct_to_device_user<Event: Send + Sync>(
+7 -6
View File
@@ -1,7 +1,7 @@
use std::time::Duration; use std::time::Duration;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result}; use conduwuit::{Error, Result};
use futures::{FutureExt, StreamExt, TryFutureExt}; use futures::{FutureExt, StreamExt, TryFutureExt};
use ruma::api::{ use ruma::api::{
client::error::ErrorKind, client::error::ErrorKind,
@@ -24,7 +24,7 @@ pub(crate) async fn get_devices_route(
body: Ruma<get_devices::v1::Request>, body: Ruma<get_devices::v1::Request>,
) -> Result<get_devices::v1::Response> { ) -> Result<get_devices::v1::Response> {
if !services.globals.user_is_local(&body.user_id) { if !services.globals.user_is_local(&body.user_id) {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Tried to access user from other server.", "Tried to access user from other server.",
)); ));
@@ -86,9 +86,10 @@ pub(crate) async fn get_keys_route(
.iter() .iter()
.any(|(u, _)| !services.globals.user_is_local(u)) .any(|(u, _)| !services.globals.user_is_local(u))
{ {
return Err!( return Err(Error::BadRequest(
BadRequest(ErrorKind::InvalidParam, "User does not belong to this server.",) ErrorKind::InvalidParam,
); "User does not belong to this server.",
));
} }
let result = get_keys_helper( let result = get_keys_helper(
@@ -120,7 +121,7 @@ pub(crate) async fn claim_keys_route(
.iter() .iter()
.any(|(u, _)| !services.globals.user_is_local(u)) .any(|(u, _)| !services.globals.user_is_local(u))
{ {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Tried to access user from other server.", "Tried to access user from other server.",
)); ));
+3 -3
View File
@@ -1,6 +1,6 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result}; use conduwuit::{Error, Result};
use ruma::api::federation::discovery::discover_homeserver; use ruma::api::{client::error::ErrorKind, federation::discovery::discover_homeserver};
use crate::Ruma; use crate::Ruma;
@@ -14,7 +14,7 @@ pub(crate) async fn well_known_server(
Ok(discover_homeserver::Response { Ok(discover_homeserver::Response {
server: match services.server.config.well_known.server.as_ref() { server: match services.server.config.well_known.server.as_ref() {
| Some(server_name) => server_name.to_owned(), | Some(server_name) => server_name.to_owned(),
| None => return Err!(BadRequest(ErrorKind::NotFound, "Not found.")), | None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
}, },
}) })
} }
+1 -2
View File
@@ -98,8 +98,7 @@ serde-saphyr.workspace = true
serde.workspace = true serde.workspace = true
smallvec.workspace = true smallvec.workspace = true
smallstr.workspace = true smallstr.workspace = true
snafu.workspace = true thiserror.workspace = true
paste.workspace = true
tikv-jemallocator.optional = true tikv-jemallocator.optional = true
tikv-jemallocator.workspace = true tikv-jemallocator.workspace = true
tikv-jemalloc-ctl.optional = true tikv-jemalloc-ctl.optional = true
+92 -17
View File
@@ -368,6 +368,31 @@ pub struct Config {
#[serde(default = "default_max_fetch_prev_events")] #[serde(default = "default_max_fetch_prev_events")]
pub max_fetch_prev_events: u16, pub max_fetch_prev_events: u16,
/// How many incoming federation transactions the server is willing to be
/// processing at any given time before it becomes overloaded and starts
/// rejecting further transactions until some slots become available.
///
/// Setting this value too low or too high may result in unstable
/// federation, and setting it too high may cause runaway resource usage.
///
/// default: 150
#[serde(default = "default_max_concurrent_inbound_transactions")]
pub max_concurrent_inbound_transactions: usize,
/// Maximum age (in seconds) for cached federation transaction responses.
/// Entries older than this will be removed during cleanup.
///
/// default: 7200 (2 hours)
#[serde(default = "default_transaction_id_cache_max_age_secs")]
pub transaction_id_cache_max_age_secs: u64,
/// Maximum number of cached federation transaction responses.
/// When the cache exceeds this limit, older entries will be removed.
///
/// default: 8192
#[serde(default = "default_transaction_id_cache_max_entries")]
pub transaction_id_cache_max_entries: usize,
/// Default/base connection timeout (seconds). This is used only by URL /// Default/base connection timeout (seconds). This is used only by URL
/// previews and update/news endpoint checks. /// previews and update/news endpoint checks.
/// ///
@@ -653,12 +678,6 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub allow_public_room_directory_over_federation: bool, pub allow_public_room_directory_over_federation: bool,
/// Set this to true to allow your server's public room directory to be
/// queried without client authentication (access token) through the Client
/// APIs. Set this to false to protect against /publicRooms spiders.
#[serde(default)]
pub allow_public_room_directory_without_auth: bool,
/// Allow guests/unauthenticated users to access TURN credentials. /// Allow guests/unauthenticated users to access TURN credentials.
/// ///
/// This is the equivalent of Synapse's `turn_allow_guests` config option. /// This is the equivalent of Synapse's `turn_allow_guests` config option.
@@ -2049,6 +2068,16 @@ pub struct Config {
pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure: pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure:
bool, bool,
/// Forcibly disables first-run mode.
///
/// This is intended to be used for Complement testing to allow the test
/// suite to register users, because first-run mode interferes with open
/// registration.
///
/// display: hidden
#[serde(default)]
pub force_disable_first_run_mode: bool,
/// display: nested /// display: nested
#[serde(default)] #[serde(default)]
pub ldap: LdapConfig, pub ldap: LdapConfig,
@@ -2061,6 +2090,12 @@ pub struct Config {
/// display: nested /// display: nested
#[serde(default)] #[serde(default)]
pub blurhashing: BlurhashConfig, pub blurhashing: BlurhashConfig,
/// Configuration for MatrixRTC (MSC4143) transport discovery.
/// display: nested
#[serde(default)]
pub matrix_rtc: MatrixRtcConfig,
#[serde(flatten)] #[serde(flatten)]
#[allow(clippy::zero_sized_map_values)] #[allow(clippy::zero_sized_map_values)]
// this is a catchall, the map shouldn't be zero at runtime // this is a catchall, the map shouldn't be zero at runtime
@@ -2126,17 +2161,16 @@ pub struct WellKnownConfig {
/// listed. /// listed.
pub support_mxid: Option<OwnedUserId>, pub support_mxid: Option<OwnedUserId>,
/// A list of MatrixRTC foci URLs which will be served as part of the /// **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
/// MSC4143 client endpoint at /.well-known/matrix/client. If you're
/// setting up livekit, you'd want something like:
/// rtc_focus_server_urls = [
/// { type = "livekit", livekit_service_url = "https://livekit.example.com" },
/// ]
/// ///
/// To disable, set this to be an empty vector (`[]`). /// A list of MatrixRTC foci URLs which will be served as part of the
/// MSC4143 client endpoint at /.well-known/matrix/client.
///
/// This option is deprecated and will be removed in a future release.
/// Please migrate to the new `[global.matrix_rtc]` config section.
/// ///
/// default: [] /// default: []
#[serde(default = "default_rtc_focus_urls")] #[serde(default)]
pub rtc_focus_server_urls: Vec<RtcFocusInfo>, pub rtc_focus_server_urls: Vec<RtcFocusInfo>,
} }
@@ -2165,6 +2199,43 @@ pub struct BlurhashConfig {
pub blurhash_max_raw_size: u64, pub blurhash_max_raw_size: u64,
} }
#[derive(Clone, Debug, Deserialize, Default)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.matrix_rtc")]
pub struct MatrixRtcConfig {
/// A list of MatrixRTC foci (transports) which will be served via the
/// MSC4143 RTC transports endpoint at
/// `/_matrix/client/v1/rtc/transports`. If you're setting up livekit,
/// you'd want something like:
/// ```toml
/// [global.matrix_rtc]
/// foci = [
/// { type = "livekit", livekit_service_url = "https://livekit.example.com" },
/// ]
/// ```
///
/// To disable, set this to an empty list (`[]`).
///
/// default: []
#[serde(default)]
pub foci: Vec<RtcFocusInfo>,
}
impl MatrixRtcConfig {
/// Returns the effective foci, falling back to the deprecated
/// `rtc_focus_server_urls` if the new config is empty.
#[must_use]
pub fn effective_foci<'a>(
&'a self,
deprecated_foci: &'a [RtcFocusInfo],
) -> &'a [RtcFocusInfo] {
if !self.foci.is_empty() {
&self.foci
} else {
deprecated_foci
}
}
}
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")] #[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")]
pub struct LdapConfig { pub struct LdapConfig {
@@ -2358,6 +2429,7 @@ const DEPRECATED_KEYS: &[&str] = &[
"well_known_support_email", "well_known_support_email",
"well_known_support_mxid", "well_known_support_mxid",
"registration_token_file", "registration_token_file",
"well_known.rtc_focus_server_urls",
]; ];
impl Config { impl Config {
@@ -2540,6 +2612,12 @@ fn default_pusher_idle_timeout() -> u64 { 15 }
fn default_max_fetch_prev_events() -> u16 { 192_u16 } fn default_max_fetch_prev_events() -> u16 { 192_u16 }
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
fn default_transaction_id_cache_max_age_secs() -> u64 { 60 * 60 * 2 }
fn default_transaction_id_cache_max_entries() -> usize { 8192 }
fn default_tracing_flame_filter() -> String { fn default_tracing_flame_filter() -> String {
cfg!(debug_assertions) cfg!(debug_assertions)
.then_some("trace,h2=off") .then_some("trace,h2=off")
@@ -2635,9 +2713,6 @@ fn default_rocksdb_stats_level() -> u8 { 1 }
#[inline] #[inline]
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V11 } pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V11 }
#[must_use]
pub fn default_rtc_focus_urls() -> Vec<RtcFocusInfo> { vec![] }
fn default_ip_range_denylist() -> Vec<String> { fn default_ip_range_denylist() -> Vec<String> {
vec![ vec![
"127.0.0.0/8".to_owned(), "127.0.0.0/8".to_owned(),
+30 -129
View File
@@ -45,162 +45,63 @@ macro_rules! Err {
macro_rules! err { macro_rules! err {
(Request(Forbidden($level:ident!($($args:tt)+)))) => {{ (Request(Forbidden($level:ident!($($args:tt)+)))) => {{
let mut buf = String::new(); let mut buf = String::new();
$crate::error::Error::Request { $crate::error::Error::Request(
kind: $crate::ruma::api::client::error::ErrorKind::forbidden(), $crate::ruma::api::client::error::ErrorKind::forbidden(),
message: $crate::err_log!(buf, $level, $($args)+), $crate::err_log!(buf, $level, $($args)+),
code: $crate::http::StatusCode::BAD_REQUEST, $crate::http::StatusCode::BAD_REQUEST
backtrace: Some($crate::snafu::Backtrace::capture()), )
}
}}; }};
(Request(Forbidden($($args:tt)+))) => { (Request(Forbidden($($args:tt)+))) => {
{ $crate::error::Error::Request(
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+); $crate::ruma::api::client::error::ErrorKind::forbidden(),
$crate::error::Error::Request { $crate::format_maybe!($($args)+),
kind: $crate::ruma::api::client::error::ErrorKind::forbidden(), $crate::http::StatusCode::BAD_REQUEST
message, )
code: $crate::http::StatusCode::BAD_REQUEST,
backtrace: Some($crate::snafu::Backtrace::capture()),
}
}
};
(Request(NotFound($level:ident!($($args:tt)+)))) => {{
let mut buf = String::new();
$crate::error::Error::Request {
kind: $crate::ruma::api::client::error::ErrorKind::NotFound,
message: $crate::err_log!(buf, $level, $($args)+),
code: $crate::http::StatusCode::BAD_REQUEST,
backtrace: None,
}
}};
(Request(NotFound($($args:tt)+))) => {
{
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::error::Error::Request {
kind: $crate::ruma::api::client::error::ErrorKind::NotFound,
message,
code: $crate::http::StatusCode::BAD_REQUEST,
backtrace: None,
}
}
}; };
(Request($variant:ident($level:ident!($($args:tt)+)))) => {{ (Request($variant:ident($level:ident!($($args:tt)+)))) => {{
let mut buf = String::new(); let mut buf = String::new();
$crate::error::Error::Request { $crate::error::Error::Request(
kind: $crate::ruma::api::client::error::ErrorKind::$variant, $crate::ruma::api::client::error::ErrorKind::$variant,
message: $crate::err_log!(buf, $level, $($args)+), $crate::err_log!(buf, $level, $($args)+),
code: $crate::http::StatusCode::BAD_REQUEST, $crate::http::StatusCode::BAD_REQUEST
backtrace: Some($crate::snafu::Backtrace::capture()), )
}
}}; }};
(Request($variant:ident($($args:tt)+))) => { (Request($variant:ident($($args:tt)+))) => {
{ $crate::error::Error::Request(
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+); $crate::ruma::api::client::error::ErrorKind::$variant,
$crate::error::Error::Request { $crate::format_maybe!($($args)+),
kind: $crate::ruma::api::client::error::ErrorKind::$variant, $crate::http::StatusCode::BAD_REQUEST
message, )
code: $crate::http::StatusCode::BAD_REQUEST,
backtrace: Some($crate::snafu::Backtrace::capture()),
}
}
}; };
(Config($item:literal, $($args:tt)+)) => {{ (Config($item:literal, $($args:tt)+)) => {{
let mut buf = String::new(); let mut buf = String::new();
$crate::error::ConfigSnafu { $crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+))
directive: $item,
message: $crate::err_log!(buf, error, config = %$item, $($args)+),
}.build()
}}; }};
(BadRequest(ErrorKind::NotFound, $($args:tt)+)) => {
{
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::error::Error::Request {
kind: $crate::ruma::api::client::error::ErrorKind::NotFound,
message,
code: $crate::http::StatusCode::BAD_REQUEST,
backtrace: None,
}
}
};
(BadRequest($kind:expr, $($args:tt)+)) => {
{
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::error::BadRequestSnafu {
kind: $kind,
message,
}.build()
}
};
(FeatureDisabled($($args:tt)+)) => {
{
let feature: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::error::FeatureDisabledSnafu { feature }.build()
}
};
(Federation($server:expr, $error:expr $(,)?)) => {
{
$crate::error::FederationSnafu {
server: $server,
error: $error,
}.build()
}
};
(InconsistentRoomState($message:expr, $room_id:expr $(,)?)) => {
{
$crate::error::InconsistentRoomStateSnafu {
message: $message,
room_id: $room_id,
}.build()
}
};
(Uiaa($info:expr $(,)?)) => {
{
$crate::error::UiaaSnafu {
info: $info,
}.build()
}
};
($variant:ident($level:ident!($($args:tt)+))) => {{ ($variant:ident($level:ident!($($args:tt)+))) => {{
let mut buf = String::new(); let mut buf = String::new();
$crate::paste::paste! { $crate::error::Error::$variant($crate::err_log!(buf, $level, $($args)+))
$crate::error::[<$variant Snafu>] {
message: $crate::err_log!(buf, $level, $($args)+),
}.build()
}
}}; }};
($variant:ident($($args:ident),+)) => {
$crate::error::Error::$variant($($args),+)
};
($variant:ident($($args:tt)+)) => { ($variant:ident($($args:tt)+)) => {
$crate::paste::paste! { $crate::error::Error::$variant($crate::format_maybe!($($args)+))
{
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::error::[<$variant Snafu>] { message }.build()
}
}
}; };
($level:ident!($($args:tt)+)) => {{ ($level:ident!($($args:tt)+)) => {{
let mut buf = String::new(); let mut buf = String::new();
let message: std::borrow::Cow<'static, str> = $crate::err_log!(buf, $level, $($args)+); $crate::error::Error::Err($crate::err_log!(buf, $level, $($args)+))
$crate::error::ErrSnafu { message }.build()
}}; }};
($($args:tt)+) => { ($($args:tt)+) => {
{ $crate::error::Error::Err($crate::format_maybe!($($args)+))
let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::error::ErrSnafu { message }.build()
}
}; };
} }
@@ -233,7 +134,7 @@ macro_rules! err_log {
}; };
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset_all!(__CALLSITE.metadata().fields(), $($fields)+)); ($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset_all!(__CALLSITE.metadata().fields(), $($fields)+));
std::borrow::Cow::<'static, str>::from($out) ($out).into()
}} }}
} }
+139 -448
View File
@@ -6,391 +6,151 @@ mod serde;
use std::{any::Any, borrow::Cow, convert::Infallible, sync::PoisonError}; use std::{any::Any, borrow::Cow, convert::Infallible, sync::PoisonError};
use snafu::{IntoError, prelude::*};
pub use self::{err::visit, log::*}; pub use self::{err::visit, log::*};
#[derive(Debug, Snafu)] #[derive(thiserror::Error)]
#[snafu(visibility(pub))]
pub enum Error { pub enum Error {
#[snafu(display("PANIC!"))] #[error("PANIC!")]
PanicAny { PanicAny(Box<dyn Any + Send>),
panic: Box<dyn Any + Send>, #[error("PANIC! {0}")]
backtrace: snafu::Backtrace, Panic(&'static str, Box<dyn Any + Send + 'static>),
},
#[snafu(display("PANIC! {message}"))]
Panic {
message: &'static str,
panic: Box<dyn Any + Send + 'static>,
backtrace: snafu::Backtrace,
},
// std // std
#[snafu(display("Format error: {source}"))] #[error(transparent)]
Fmt { Fmt(#[from] std::fmt::Error),
source: std::fmt::Error, #[error(transparent)]
backtrace: snafu::Backtrace, FromUtf8(#[from] std::string::FromUtf8Error),
}, #[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[snafu(display("UTF-8 conversion error: {source}"))] #[error(transparent)]
FromUtf8 { ParseFloat(#[from] std::num::ParseFloatError),
source: std::string::FromUtf8Error, #[error(transparent)]
backtrace: snafu::Backtrace, ParseInt(#[from] std::num::ParseIntError),
}, #[error(transparent)]
Std(#[from] Box<dyn std::error::Error + Send>),
#[snafu(display("I/O error: {source}"))] #[error(transparent)]
Io { ThreadAccessError(#[from] std::thread::AccessError),
source: std::io::Error, #[error(transparent)]
backtrace: snafu::Backtrace, TryFromInt(#[from] std::num::TryFromIntError),
}, #[error(transparent)]
TryFromSlice(#[from] std::array::TryFromSliceError),
#[snafu(display("Parse float error: {source}"))] #[error(transparent)]
ParseFloat { Utf8(#[from] std::str::Utf8Error),
source: std::num::ParseFloatError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Parse int error: {source}"))]
ParseInt {
source: std::num::ParseIntError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Error: {source}"))]
Std {
source: Box<dyn std::error::Error + Send>,
backtrace: snafu::Backtrace,
},
#[snafu(display("Thread access error: {source}"))]
ThreadAccessError {
source: std::thread::AccessError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Integer conversion error: {source}"))]
TryFromInt {
source: std::num::TryFromIntError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Slice conversion error: {source}"))]
TryFromSlice {
source: std::array::TryFromSliceError,
backtrace: snafu::Backtrace,
},
#[snafu(display("UTF-8 error: {source}"))]
Utf8 {
source: std::str::Utf8Error,
backtrace: snafu::Backtrace,
},
// third-party // third-party
#[snafu(display("Capacity error: {source}"))] #[error(transparent)]
CapacityError { CapacityError(#[from] arrayvec::CapacityError),
source: arrayvec::CapacityError, #[error(transparent)]
backtrace: snafu::Backtrace, CargoToml(#[from] cargo_toml::Error),
}, #[error(transparent)]
Clap(#[from] clap::error::Error),
#[snafu(display("Cargo.toml error: {source}"))] #[error(transparent)]
CargoToml { Extension(#[from] axum::extract::rejection::ExtensionRejection),
source: cargo_toml::Error, #[error(transparent)]
backtrace: snafu::Backtrace, Figment(#[from] figment::error::Error),
}, #[error(transparent)]
Http(#[from] http::Error),
#[snafu(display("Clap error: {source}"))] #[error(transparent)]
Clap { HttpHeader(#[from] http::header::InvalidHeaderValue),
source: clap::error::Error, #[error("Join error: {0}")]
backtrace: snafu::Backtrace, JoinError(#[from] tokio::task::JoinError),
}, #[error(transparent)]
Json(#[from] serde_json::Error),
#[snafu(display("Extension rejection: {source}"))] #[error(transparent)]
Extension { JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export
source: axum::extract::rejection::ExtensionRejection, #[error(transparent)]
backtrace: snafu::Backtrace, JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export
}, #[error(transparent)]
Path(#[from] axum::extract::rejection::PathRejection),
#[snafu(display("Figment error: {source}"))] #[error("Mutex poisoned: {0}")]
Figment { Poison(Cow<'static, str>),
source: figment::error::Error, #[error("Regex error: {0}")]
backtrace: snafu::Backtrace, Regex(#[from] regex::Error),
}, #[error("Request error: {0}")]
Reqwest(#[from] reqwest::Error),
#[snafu(display("HTTP error: {source}"))] #[error("{0}")]
Http { SerdeDe(Cow<'static, str>),
source: http::Error, #[error("{0}")]
backtrace: snafu::Backtrace, SerdeSer(Cow<'static, str>),
}, #[error(transparent)]
TomlDe(#[from] toml::de::Error),
#[snafu(display("Invalid HTTP header value: {source}"))] #[error(transparent)]
HttpHeader { TomlSer(#[from] toml::ser::Error),
source: http::header::InvalidHeaderValue, #[error("Tracing filter error: {0}")]
backtrace: snafu::Backtrace, TracingFilter(#[from] tracing_subscriber::filter::ParseError),
}, #[error("Tracing reload error: {0}")]
TracingReload(#[from] tracing_subscriber::reload::Error),
#[snafu(display("Join error: {source}"))] #[error(transparent)]
JoinError { TypedHeader(#[from] axum_extra::typed_header::TypedHeaderRejection),
source: tokio::task::JoinError, #[error(transparent)]
backtrace: snafu::Backtrace, YamlDe(#[from] serde_saphyr::Error),
}, #[error(transparent)]
YamlSer(#[from] serde_saphyr::ser_error::Error),
#[snafu(display("JSON error: {source}"))]
Json {
source: serde_json::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("JS parse int error: {source}"))]
JsParseInt {
source: ruma::JsParseIntError,
backtrace: snafu::Backtrace,
},
#[snafu(display("JS try from int error: {source}"))]
JsTryFromInt {
source: ruma::JsTryFromIntError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Path rejection: {source}"))]
Path {
source: axum::extract::rejection::PathRejection,
backtrace: snafu::Backtrace,
},
#[snafu(display("Mutex poisoned: {message}"))]
Poison {
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
#[snafu(display("Regex error: {source}"))]
Regex {
source: regex::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("Request error: {source}"))]
Reqwest {
source: reqwest::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("{message}"))]
SerdeDe {
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
#[snafu(display("{message}"))]
SerdeSer {
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
#[snafu(display("TOML deserialization error: {source}"))]
TomlDe {
source: toml::de::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("TOML serialization error: {source}"))]
TomlSer {
source: toml::ser::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("Tracing filter error: {source}"))]
TracingFilter {
source: tracing_subscriber::filter::ParseError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Tracing reload error: {source}"))]
TracingReload {
source: tracing_subscriber::reload::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("Typed header rejection: {source}"))]
TypedHeader {
source: axum_extra::typed_header::TypedHeaderRejection,
backtrace: snafu::Backtrace,
},
#[snafu(display("YAML deserialization error: {source}"))]
YamlDe {
source: serde_saphyr::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("YAML serialization error: {source}"))]
YamlSer {
source: serde_saphyr::ser_error::Error,
backtrace: snafu::Backtrace,
},
// ruma/conduwuit // ruma/conduwuit
#[snafu(display("Arithmetic operation failed: {message}"))] #[error("Arithmetic operation failed: {0}")]
Arithmetic { Arithmetic(Cow<'static, str>),
message: Cow<'static, str>, #[error("{0}: {1}")]
backtrace: snafu::Backtrace, BadRequest(ruma::api::client::error::ErrorKind, &'static str), //TODO: remove
}, #[error("{0}")]
BadServerResponse(Cow<'static, str>),
#[snafu(display("{kind}: {message}"))] #[error(transparent)]
BadRequest { CanonicalJson(#[from] ruma::CanonicalJsonError),
kind: ruma::api::client::error::ErrorKind, #[error("There was a problem with the '{0}' directive in your configuration: {1}")]
message: Cow<'static, str>, Config(&'static str, Cow<'static, str>),
backtrace: snafu::Backtrace, #[error("{0}")]
}, Conflict(Cow<'static, str>), // This is only needed for when a room alias already exists
#[error(transparent)]
#[snafu(display("{message}"))] ContentDisposition(#[from] ruma::http_headers::ContentDispositionParseError),
BadServerResponse { #[error("{0}")]
message: Cow<'static, str>, Database(Cow<'static, str>),
backtrace: snafu::Backtrace, #[error("Feature '{0}' is not available on this server.")]
}, FeatureDisabled(Cow<'static, str>),
#[error("Remote server {0} responded with: {1}")]
#[snafu(display("Canonical JSON error: {source}"))] Federation(ruma::OwnedServerName, ruma::api::client::error::Error),
CanonicalJson { #[error("{0} in {1}")]
source: ruma::CanonicalJsonError, InconsistentRoomState(&'static str, ruma::OwnedRoomId),
backtrace: snafu::Backtrace, #[error(transparent)]
}, IntoHttp(#[from] ruma::api::error::IntoHttpError),
#[error("{0}")]
#[snafu(display( Ldap(Cow<'static, str>),
"There was a problem with the '{directive}' directive in your configuration: {message}" #[error(transparent)]
))] Mxc(#[from] ruma::MxcUriError),
Config { #[error(transparent)]
directive: &'static str, Mxid(#[from] ruma::IdParseError),
message: Cow<'static, str>, #[error("from {0}: {1}")]
backtrace: snafu::Backtrace, Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
}, #[error("{0}: {1}")]
Request(ruma::api::client::error::ErrorKind, Cow<'static, str>, http::StatusCode),
#[snafu(display("{message}"))] #[error(transparent)]
Conflict { Ruma(#[from] ruma::api::client::error::Error),
message: Cow<'static, str>, #[error(transparent)]
backtrace: snafu::Backtrace, Signatures(#[from] ruma::signatures::Error),
}, #[error(transparent)]
StateRes(#[from] crate::state_res::Error),
#[snafu(display("Content disposition error: {source}"))] #[error("uiaa")]
ContentDisposition { Uiaa(ruma::api::client::uiaa::UiaaInfo),
source: ruma::http_headers::ContentDispositionParseError,
backtrace: snafu::Backtrace,
},
#[snafu(display("{message}"))]
Database {
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
#[snafu(display("Feature '{feature}' is not available on this server."))]
FeatureDisabled {
feature: Cow<'static, str>,
},
#[snafu(display("Remote server {server} responded with: {error}"))]
Federation {
server: ruma::OwnedServerName,
error: ruma::api::client::error::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("{message} in {room_id}"))]
InconsistentRoomState {
message: &'static str,
room_id: ruma::OwnedRoomId,
backtrace: snafu::Backtrace,
},
#[snafu(display("HTTP conversion error: {source}"))]
IntoHttp {
source: ruma::api::error::IntoHttpError,
backtrace: snafu::Backtrace,
},
#[snafu(display("{message}"))]
Ldap {
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
#[snafu(display("MXC URI error: {source}"))]
Mxc {
source: ruma::MxcUriError,
backtrace: snafu::Backtrace,
},
#[snafu(display("Matrix ID parse error: {source}"))]
Mxid {
source: ruma::IdParseError,
backtrace: snafu::Backtrace,
},
#[snafu(display("from {server}: {error}"))]
Redaction {
server: ruma::OwnedServerName,
error: ruma::canonical_json::RedactionError,
backtrace: snafu::Backtrace,
},
#[snafu(display("{kind}: {message}"))]
Request {
kind: ruma::api::client::error::ErrorKind,
message: Cow<'static, str>,
code: http::StatusCode,
backtrace: Option<snafu::Backtrace>,
},
#[snafu(display("Ruma error: {source}"))]
Ruma {
source: ruma::api::client::error::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("Signature error: {source}"))]
Signatures {
source: ruma::signatures::Error,
backtrace: snafu::Backtrace,
},
#[snafu(display("State resolution error: {source}"))]
#[snafu(context(false))]
StateRes {
source: crate::state_res::Error,
},
#[snafu(display("uiaa"))]
Uiaa {
info: ruma::api::client::uiaa::UiaaInfo,
},
// unique / untyped // unique / untyped
#[snafu(display("{message}"))] #[error("{0}")]
Err { Err(Cow<'static, str>),
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
} }
impl Error { impl Error {
#[inline] #[inline]
#[must_use] #[must_use]
pub fn from_errno() -> Self { IoSnafu {}.into_error(std::io::Error::last_os_error()) } pub fn from_errno() -> Self { Self::Io(std::io::Error::last_os_error()) }
//#[deprecated] //#[deprecated]
#[must_use]
pub fn bad_database(message: &'static str) -> Self { pub fn bad_database(message: &'static str) -> Self {
let message: Cow<'static, str> = message.into(); crate::err!(Database(error!("{message}")))
DatabaseSnafu { message }.build()
} }
/// Sanitizes public-facing errors that can leak sensitive information. /// Sanitizes public-facing errors that can leak sensitive information.
pub fn sanitized_message(&self) -> String { pub fn sanitized_message(&self) -> String {
match self { match self {
| Self::Database { .. } => String::from("Database error occurred."), | Self::Database(..) => String::from("Database error occurred."),
| Self::Io { .. } => String::from("I/O error occurred."), | Self::Io(..) => String::from("I/O error occurred."),
| _ => self.message(), | _ => self.message(),
} }
} }
@@ -398,8 +158,8 @@ impl Error {
/// Generate the error message string. /// Generate the error message string.
pub fn message(&self) -> String { pub fn message(&self) -> String {
match self { match self {
| Self::Federation { server, error, .. } => format!("Answer from {server}: {error}"), | Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
| Self::Ruma { source, .. } => response::ruma_error_message(source), | Self::Ruma(error) => response::ruma_error_message(error),
| _ => format!("{self}"), | _ => format!("{self}"),
} }
} }
@@ -410,10 +170,10 @@ impl Error {
use ruma::api::client::error::ErrorKind::{FeatureDisabled, Unknown}; use ruma::api::client::error::ErrorKind::{FeatureDisabled, Unknown};
match self { match self {
| Self::Federation { error, .. } => response::ruma_error_kind(error).clone(), | Self::Federation(_, error) | Self::Ruma(error) =>
| Self::Ruma { source, .. } => response::ruma_error_kind(source).clone(), response::ruma_error_kind(error).clone(),
| Self::BadRequest { kind, .. } | Self::Request { kind, .. } => kind.clone(), | Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
| Self::FeatureDisabled { .. } => FeatureDisabled, | Self::FeatureDisabled(..) => FeatureDisabled,
| _ => Unknown, | _ => Unknown,
} }
} }
@@ -424,15 +184,13 @@ impl Error {
use http::StatusCode; use http::StatusCode;
match self { match self {
| Self::Federation { error, .. } => error.status_code, | Self::Federation(_, error) | Self::Ruma(error) => error.status_code,
| Self::Ruma { source, .. } => source.status_code, | Self::Request(kind, _, code) => response::status_code(kind, *code),
| Self::Request { kind, code, .. } => response::status_code(kind, *code), | Self::BadRequest(kind, ..) => response::bad_request_code(kind),
| Self::BadRequest { kind, .. } => response::bad_request_code(kind), | Self::FeatureDisabled(..) => response::bad_request_code(&self.kind()),
| Self::FeatureDisabled { .. } => response::bad_request_code(&self.kind()), | Self::Reqwest(error) => error.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
| Self::Reqwest { source, .. } => | Self::Conflict(_) => StatusCode::CONFLICT,
source.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), | Self::Io(error) => response::io_error_code(error.kind()),
| Self::Conflict { .. } => StatusCode::CONFLICT,
| Self::Io { source, .. } => response::io_error_code(source.kind()),
| _ => StatusCode::INTERNAL_SERVER_ERROR, | _ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
@@ -445,46 +203,16 @@ impl Error {
pub fn is_not_found(&self) -> bool { self.status_code() == http::StatusCode::NOT_FOUND } pub fn is_not_found(&self) -> bool { self.status_code() == http::StatusCode::NOT_FOUND }
} }
// Debug is already derived by Snafu impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// Macro to reduce boilerplate for From implementations using Snafu context write!(f, "{}", self.message())
macro_rules! impl_from_snafu { }
($source_ty:ty => $context:ident) => {
impl From<$source_ty> for Error {
fn from(source: $source_ty) -> Self { $context.into_error(source) }
}
};
}
/// Macro for From impls that format messages into ErrSnafu or other
/// message-based contexts
macro_rules! impl_from_message {
($source_ty:ty => $context:ident, $msg:expr) => {
impl From<$source_ty> for Error {
fn from(source: $source_ty) -> Self {
let message: Cow<'static, str> = format!($msg, source).into();
$context { message }.build()
}
}
};
}
/// Macro for From impls with constant messages (no formatting)
macro_rules! impl_from_const_message {
($source_ty:ty => $context:ident, $msg:expr) => {
impl From<$source_ty> for Error {
fn from(_source: $source_ty) -> Self {
let message: Cow<'static, str> = $msg.into();
$context { message }.build()
}
}
};
} }
impl<T> From<PoisonError<T>> for Error { impl<T> From<PoisonError<T>> for Error {
#[cold] #[cold]
#[inline(never)] #[inline(never)]
fn from(e: PoisonError<T>) -> Self { PoisonSnafu { message: e.to_string() }.build() } fn from(e: PoisonError<T>) -> Self { Self::Poison(e.to_string().into()) }
} }
#[allow(clippy::fallible_impl_from)] #[allow(clippy::fallible_impl_from)]
@@ -496,43 +224,6 @@ impl From<Infallible> for Error {
} }
} }
// Implementations using the macro
impl_from_snafu!(std::io::Error => IoSnafu);
impl_from_snafu!(std::string::FromUtf8Error => FromUtf8Snafu);
impl_from_snafu!(regex::Error => RegexSnafu);
impl_from_snafu!(ruma::http_headers::ContentDispositionParseError => ContentDispositionSnafu);
impl_from_snafu!(ruma::api::error::IntoHttpError => IntoHttpSnafu);
impl_from_snafu!(ruma::JsTryFromIntError => JsTryFromIntSnafu);
impl_from_snafu!(ruma::CanonicalJsonError => CanonicalJsonSnafu);
impl_from_snafu!(axum::extract::rejection::PathRejection => PathSnafu);
impl_from_snafu!(clap::error::Error => ClapSnafu);
impl_from_snafu!(ruma::MxcUriError => MxcSnafu);
impl_from_snafu!(serde_saphyr::ser_error::Error => YamlSerSnafu);
impl_from_snafu!(toml::de::Error => TomlDeSnafu);
impl_from_snafu!(http::header::InvalidHeaderValue => HttpHeaderSnafu);
impl_from_snafu!(serde_json::Error => JsonSnafu);
// Custom implementations using message formatting
impl_from_const_message!(std::fmt::Error => ErrSnafu, "formatting error");
impl_from_message!(std::str::Utf8Error => ErrSnafu, "UTF-8 error: {}");
impl_from_message!(std::num::TryFromIntError => ArithmeticSnafu, "integer conversion error: {}");
impl_from_message!(tracing_subscriber::reload::Error => ErrSnafu, "tracing reload error: {}");
impl_from_message!(reqwest::Error => ErrSnafu, "HTTP client error: {}");
impl_from_message!(ruma::signatures::Error => ErrSnafu, "Signature error: {}");
impl_from_message!(ruma::IdParseError => ErrSnafu, "ID parse error: {}");
impl_from_message!(std::num::ParseIntError => ErrSnafu, "Integer parse error: {}");
impl_from_message!(std::array::TryFromSliceError => ErrSnafu, "Slice conversion error: {}");
impl_from_message!(tokio::task::JoinError => ErrSnafu, "Task join error: {}");
impl_from_message!(serde_saphyr::Error => ErrSnafu, "YAML error: {}");
// Generic implementation for CapacityError
impl<T> From<arrayvec::CapacityError<T>> for Error {
fn from(_source: arrayvec::CapacityError<T>) -> Self {
let message: Cow<'static, str> = "capacity error: buffer is full".into();
ErrSnafu { message }.build()
}
}
#[cold] #[cold]
#[inline(never)] #[inline(never)]
pub fn infallible(_e: &Infallible) { pub fn infallible(_e: &Infallible) {
+5 -8
View File
@@ -15,16 +15,13 @@ impl Error {
#[must_use] #[must_use]
#[inline] #[inline]
pub fn from_panic(e: Box<dyn Any + Send>) -> Self { pub fn from_panic(e: Box<dyn Any + Send>) -> Self { Self::Panic(debug::panic_str(&e), e) }
use super::PanicSnafu;
PanicSnafu { message: debug::panic_str(&e), panic: e }.build()
}
#[inline] #[inline]
pub fn into_panic(self) -> Box<dyn Any + Send + 'static> { pub fn into_panic(self) -> Box<dyn Any + Send + 'static> {
match self { match self {
| Self::Panic { panic, .. } | Self::PanicAny { panic, .. } => panic, | Self::Panic(_, e) | Self::PanicAny(e) => e,
| Self::JoinError { source, .. } => source.into_panic(), | Self::JoinError(e) => e.into_panic(),
| _ => Box::new(self), | _ => Box::new(self),
} }
} }
@@ -40,8 +37,8 @@ impl Error {
#[inline] #[inline]
pub fn is_panic(&self) -> bool { pub fn is_panic(&self) -> bool {
match &self { match &self {
| Self::Panic { .. } | Self::PanicAny { .. } => true, | Self::Panic(..) | Self::PanicAny(..) => true,
| Self::JoinError { source, .. } => source.is_panic(), | Self::JoinError(e) => e.is_panic(),
| _ => false, | _ => false,
} }
} }
+2 -2
View File
@@ -47,8 +47,8 @@ impl axum::response::IntoResponse for Error {
impl From<Error> for UiaaResponse { impl From<Error> for UiaaResponse {
#[inline] #[inline]
fn from(error: Error) -> Self { fn from(error: Error) -> Self {
if let Error::Uiaa { info, .. } = error { if let Error::Uiaa(uiaainfo) = error {
return Self::AuthResponse(info); return Self::AuthResponse(uiaainfo);
} }
let body = ErrorBody::Standard { let body = ErrorBody::Standard {
+2 -8
View File
@@ -5,15 +5,9 @@ use serde::{de, ser};
use crate::Error; use crate::Error;
impl de::Error for Error { impl de::Error for Error {
fn custom<T: Display + ToString>(msg: T) -> Self { fn custom<T: Display + ToString>(msg: T) -> Self { Self::SerdeDe(msg.to_string().into()) }
let message: std::borrow::Cow<'static, str> = msg.to_string().into();
super::SerdeDeSnafu { message }.build()
}
} }
impl ser::Error for Error { impl ser::Error for Error {
fn custom<T: Display + ToString>(msg: T) -> Self { fn custom<T: Display + ToString>(msg: T) -> Self { Self::SerdeSer(msg.to_string().into()) }
let message: std::borrow::Cow<'static, str> = msg.to_string().into();
super::SerdeSerSnafu { message }.build()
}
} }
+9
View File
@@ -14,6 +14,7 @@ static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
static VERSION: OnceLock<String> = OnceLock::new(); static VERSION: OnceLock<String> = OnceLock::new();
static VERSION_UA: OnceLock<String> = OnceLock::new(); static VERSION_UA: OnceLock<String> = OnceLock::new();
static USER_AGENT: OnceLock<String> = OnceLock::new(); static USER_AGENT: OnceLock<String> = OnceLock::new();
static USER_AGENT_MEDIA: OnceLock<String> = OnceLock::new();
#[inline] #[inline]
#[must_use] #[must_use]
@@ -21,14 +22,22 @@ pub fn name() -> &'static str { BRANDING }
#[inline] #[inline]
pub fn version() -> &'static str { VERSION.get_or_init(init_version) } pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
#[inline] #[inline]
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) } pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
#[inline] #[inline]
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) } pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
#[inline]
pub fn user_agent_media() -> &'static str { USER_AGENT_MEDIA.get_or_init(init_user_agent_media) }
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) } fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
fn init_user_agent_media() -> String {
format!("{}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", name(), version_ua())
}
fn init_version_ua() -> String { fn init_version_ua() -> String {
conduwuit_build_metadata::version_tag() conduwuit_build_metadata::version_tag()
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}")) .map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
+3 -10
View File
@@ -1,7 +1,7 @@
use ruma::{RoomVersionId, canonical_json::redact_content_in_place}; use ruma::{RoomVersionId, canonical_json::redact_content_in_place};
use serde_json::{Value as JsonValue, json, value::to_raw_value}; use serde_json::{Value as JsonValue, json, value::to_raw_value};
use crate::{Result, err, implement}; use crate::{Error, Result, err, implement};
#[implement(super::Pdu)] #[implement(super::Pdu)]
pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> Result { pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> Result {
@@ -10,15 +10,8 @@ pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) ->
let mut content = serde_json::from_str(self.content.get()) let mut content = serde_json::from_str(self.content.get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?; .map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
redact_content_in_place(&mut content, room_version_id, self.kind.to_string()).map_err( redact_content_in_place(&mut content, room_version_id, self.kind.to_string())
|error| { .map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
crate::error::RedactionSnafu {
server: self.sender.server_name().to_owned(),
error,
}
.build()
},
)?;
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason"); let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
+5 -7
View File
@@ -27,7 +27,7 @@ use serde_json::{
use crate::{ use crate::{
matrix::{Event, Pdu, pdu::EventHash}, matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap, error::NotFoundSnafu}, state_res::{self as state_res, Error, Result, StateMap},
}; };
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
@@ -170,12 +170,10 @@ struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
#[allow(unused)] #[allow(unused)]
impl<E: Event + Clone> TestStore<E> { impl<E: Event + Clone> TestStore<E> {
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> { fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
self.0.get(event_id).cloned().ok_or_else(|| { self.0
NotFoundSnafu { .get(event_id)
message: format!("{} not found", event_id), .cloned()
} .ok_or_else(|| Error::NotFound(format!("{} not found", event_id)))
.build()
})
} }
/// Returns the events that correspond to the `event_ids` sorted in the same /// Returns the events that correspond to the `event_ids` sorted in the same
+10 -27
View File
@@ -1,40 +1,23 @@
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use snafu::{IntoError, prelude::*}; use thiserror::Error;
/// Represents the various errors that arise when resolving state. /// Represents the various errors that arise when resolving state.
#[derive(Debug, Snafu)] #[derive(Error, Debug)]
#[snafu(visibility(pub))]
#[non_exhaustive] #[non_exhaustive]
pub enum Error { pub enum Error {
/// A deserialization error. /// A deserialization error.
#[snafu(display("JSON error: {source}"))] #[error(transparent)]
SerdeJson { SerdeJson(#[from] JsonError),
source: JsonError,
backtrace: snafu::Backtrace,
},
/// The given option or version is unsupported. /// The given option or version is unsupported.
#[snafu(display("Unsupported room version: {version}"))] #[error("Unsupported room version: {0}")]
Unsupported { Unsupported(String),
version: String,
backtrace: snafu::Backtrace,
},
/// The given event was not found. /// The given event was not found.
#[snafu(display("Not found error: {message}"))] #[error("Not found error: {0}")]
NotFound { NotFound(String),
message: String,
backtrace: snafu::Backtrace,
},
/// Invalid fields in the given PDU. /// Invalid fields in the given PDU.
#[snafu(display("Invalid PDU: {message}"))] #[error("Invalid PDU: {0}")]
InvalidPdu { InvalidPdu(String),
message: String,
backtrace: snafu::Backtrace,
},
}
impl From<serde_json::Error> for Error {
fn from(source: serde_json::Error) -> Self { SerdeJsonSnafu.into_error(source) }
} }
+3 -4
View File
@@ -24,7 +24,6 @@ use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue};
use super::{ use super::{
Error, Event, Result, StateEventType, StateKey, TimelineEventType, Error, Event, Result, StateEventType, StateKey, TimelineEventType,
error::InvalidPduSnafu,
power_levels::{ power_levels::{
deserialize_power_levels, deserialize_power_levels_content_fields, deserialize_power_levels, deserialize_power_levels_content_fields,
deserialize_power_levels_content_invite, deserialize_power_levels_content_redact, deserialize_power_levels_content_invite, deserialize_power_levels_content_redact,
@@ -384,8 +383,8 @@ where
return Ok(false); return Ok(false);
} }
let target_user = <&UserId>::try_from(state_key) let target_user =
.map_err(|e| InvalidPduSnafu { message: format!("{e}") }.build())?; <&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{e}")))?;
let user_for_join_auth = content let user_for_join_auth = content
.join_authorised_via_users_server .join_authorised_via_users_server
@@ -462,7 +461,7 @@ where
?sender_membership_event_content, ?sender_membership_event_content,
"Sender membership event content missing membership field" "Sender membership event content missing membership field"
); );
return Err(InvalidPduSnafu { message: "Missing membership field" }.build()); return Err(Error::InvalidPdu("Missing membership field".to_owned()));
}; };
let membership_state = membership_state.deserialize()?; let membership_state = membership_state.deserialize()?;
+29 -41
View File
@@ -29,18 +29,18 @@ use ruma::{
}; };
use serde_json::from_str as from_json_str; use serde_json::from_str as from_json_str;
pub(crate) use self::error::{Error, InvalidPduSnafu, NotFoundSnafu}; pub(crate) use self::error::Error;
use self::power_levels::PowerLevelsContentFields; use self::power_levels::PowerLevelsContentFields;
pub use self::{ pub use self::{
event_auth::{auth_check, auth_types_for_event}, event_auth::{auth_check, auth_types_for_event},
room_version::RoomVersion, room_version::RoomVersion,
}; };
use super::{Event, StateKey};
use crate::{ use crate::{
debug, debug_error, debug, debug_error, err,
matrix::{Event, StateKey},
state_res::room_version::StateResolutionVersion, state_res::room_version::StateResolutionVersion,
trace, trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt}, utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
warn, warn,
}; };
@@ -118,10 +118,7 @@ where
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch) let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
.await .await
.ok_or_else(|| { .ok_or_else(|| {
InvalidPduSnafu { Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
message: "Failed to calculate conflicted subgraph",
}
.build()
})?; })?;
debug!(count = csg.len(), "conflicted subgraph"); debug!(count = csg.len(), "conflicted subgraph");
trace!(set = ?csg, "conflicted subgraph"); trace!(set = ?csg, "conflicted subgraph");
@@ -152,11 +149,10 @@ where
let control_events: Vec<_> = all_conflicted let control_events: Vec<_> = all_conflicted
.iter() .iter()
.stream() .stream()
.broad_filter_map(async |id| { .wide_filter_map(async |id| {
event_fetch(id.clone()) is_power_event_id(id, &event_fetch)
.await .await
.filter(|event| is_power_event(&event)) .then_some(id.clone())
.map(|_| id.clone())
}) })
.collect() .collect()
.await; .await;
@@ -318,10 +314,7 @@ where
trace!(event_id = event_id.as_str(), "fetching event for its auth events"); trace!(event_id = event_id.as_str(), "fetching event for its auth events");
let evt = fetch_event(event_id.clone()).await; let evt = fetch_event(event_id.clone()).await;
if evt.is_none() { if evt.is_none() {
tracing::error!( err!("could not fetch event {} to calculate conflicted subgraph", event_id);
"could not fetch event {} to calculate conflicted subgraph",
event_id
);
path.pop(); path.pop();
continue; continue;
} }
@@ -409,11 +402,11 @@ where
let fetcher = async |event_id: OwnedEventId| { let fetcher = async |event_id: OwnedEventId| {
let pl = *event_to_pl let pl = *event_to_pl
.get(&event_id) .get(&event_id)
.ok_or_else(|| NotFoundSnafu { message: "" }.build())?; .ok_or_else(|| Error::NotFound(String::new()))?;
let ev = fetch_event(event_id) let ev = fetch_event(event_id)
.await .await
.ok_or_else(|| NotFoundSnafu { message: "" }.build())?; .ok_or_else(|| Error::NotFound(String::new()))?;
Ok((pl, ev.origin_server_ts())) Ok((pl, ev.origin_server_ts()))
}; };
@@ -619,12 +612,9 @@ where
let events_to_check: Vec<_> = events_to_check let events_to_check: Vec<_> = events_to_check
.map(Result::Ok) .map(Result::Ok)
.broad_and_then(async |event_id| { .broad_and_then(async |event_id| {
fetch_event(event_id.to_owned()).await.ok_or_else(|| { fetch_event(event_id.to_owned())
NotFoundSnafu { .await
message: format!("Failed to find {event_id}"), .ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}")))
}
.build()
})
}) })
.try_collect() .try_collect()
.boxed() .boxed()
@@ -663,7 +653,7 @@ where
trace!(event_id = event.event_id().as_str(), "checking event"); trace!(event_id = event.event_id().as_str(), "checking event");
let state_key = event let state_key = event
.state_key() .state_key()
.ok_or_else(|| InvalidPduSnafu { message: "State event had no state key" }.build())?; .ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
let auth_types = auth_types_for_event( let auth_types = auth_types_for_event(
event.event_type(), event.event_type(),
@@ -679,14 +669,13 @@ where
trace!("room version uses hashed IDs, manually fetching create event"); trace!("room version uses hashed IDs, manually fetching create event");
let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$"); let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$");
let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| { let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| {
InvalidPduSnafu { Error::InvalidPdu(format!(
message: format!("Failed to parse create event ID from room ID/hash: {e}"), "Failed to parse create event ID from room ID/hash: {e}"
} ))
.build()
})?;
let create_event = fetch_event(create_event_id.into()).await.ok_or_else(|| {
NotFoundSnafu { message: "Failed to find create event" }.build()
})?; })?;
let create_event = fetch_event(create_event_id.into())
.await
.ok_or_else(|| Error::NotFound("Failed to find create event".into()))?;
auth_state.insert(create_event.event_type().with_state_key(""), create_event); auth_state.insert(create_event.event_type().with_state_key(""), create_event);
} }
for aid in event.auth_events() { for aid in event.auth_events() {
@@ -697,7 +686,7 @@ where
auth_state.insert( auth_state.insert(
ev.event_type() ev.event_type()
.with_state_key(ev.state_key().ok_or_else(|| { .with_state_key(ev.state_key().ok_or_else(|| {
InvalidPduSnafu { message: "State event had no state key" }.build() Error::InvalidPdu("State event had no state key".to_owned())
})?), })?),
ev.clone(), ev.clone(),
); );
@@ -812,13 +801,13 @@ where
let event = fetch_event(p.clone()) let event = fetch_event(p.clone())
.await .await
.ok_or_else(|| NotFoundSnafu { message: format!("Failed to find {p}") }.build())?; .ok_or_else(|| Error::NotFound(format!("Failed to find {p}")))?;
pl = None; pl = None;
for aid in event.auth_events() { for aid in event.auth_events() {
let ev = fetch_event(aid.to_owned()).await.ok_or_else(|| { let ev = fetch_event(aid.to_owned())
NotFoundSnafu { message: format!("Failed to find {aid}") }.build() .await
})?; .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?;
if is_type_and_key(&ev, &TimelineEventType::RoomPowerLevels, "") { if is_type_and_key(&ev, &TimelineEventType::RoomPowerLevels, "") {
pl = Some(aid.to_owned()); pl = Some(aid.to_owned());
@@ -880,9 +869,9 @@ where
event = None; event = None;
for aid in sort_ev.auth_events() { for aid in sort_ev.auth_events() {
let aev = fetch_event(aid.to_owned()).await.ok_or_else(|| { let aev = fetch_event(aid.to_owned())
NotFoundSnafu { message: format!("Failed to find {aid}") }.build() .await
})?; .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?;
if is_type_and_key(&aev, &TimelineEventType::RoomPowerLevels, "") { if is_type_and_key(&aev, &TimelineEventType::RoomPowerLevels, "") {
event = Some(aev); event = Some(aev);
@@ -926,7 +915,6 @@ async fn add_event_and_auth_chain_to_graph<E, F, Fut>(
} }
} }
#[allow(dead_code)]
async fn is_power_event_id<E, F, Fut>(event_id: &EventId, fetch: &F) -> bool async fn is_power_event_id<E, F, Fut>(event_id: &EventId, fetch: &F) -> bool
where where
F: Fn(OwnedEventId) -> Fut + Sync, F: Fn(OwnedEventId) -> Fut + Sync,
+2 -6
View File
@@ -1,6 +1,6 @@
use ruma::RoomVersionId; use ruma::RoomVersionId;
use super::{Result, error::UnsupportedSnafu}; use super::{Error, Result};
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::exhaustive_enums)] #[allow(clippy::exhaustive_enums)]
@@ -163,11 +163,7 @@ impl RoomVersion {
| RoomVersionId::V10 => Self::V10, | RoomVersionId::V10 => Self::V10,
| RoomVersionId::V11 => Self::V11, | RoomVersionId::V11 => Self::V11,
| RoomVersionId::V12 => Self::V12, | RoomVersionId::V12 => Self::V12,
| ver => | ver => return Err(Error::Unsupported(format!("found version `{ver}`"))),
return Err(UnsupportedSnafu {
version: format!("found version `{ver}`"),
}
.build()),
}) })
} }
} }
+2 -2
View File
@@ -22,7 +22,7 @@ use serde_json::{
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
}; };
use super::{auth_types_for_event, error::NotFoundSnafu}; use super::auth_types_for_event;
use crate::{ use crate::{
Result, RoomVersion, info, Result, RoomVersion, info,
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash}, matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
@@ -232,7 +232,7 @@ impl<E: Event + Clone> TestStore<E> {
self.0 self.0
.get(event_id) .get(event_id)
.cloned() .cloned()
.ok_or_else(|| NotFoundSnafu { message: format!("{event_id} not found") }.build()) .ok_or_else(|| super::Error::NotFound(format!("{event_id} not found")))
.map_err(Into::into) .map_err(Into::into)
} }
-2
View File
@@ -14,11 +14,9 @@ pub mod utils;
pub use ::arrayvec; pub use ::arrayvec;
pub use ::http; pub use ::http;
pub use ::paste;
pub use ::ruma; pub use ::ruma;
pub use ::smallstr; pub use ::smallstr;
pub use ::smallvec; pub use ::smallvec;
pub use ::snafu;
pub use ::toml; pub use ::toml;
pub use ::tracing; pub use ::tracing;
pub use config::Config; pub use config::Config;
+7 -9
View File
@@ -3,19 +3,17 @@ use futures::{
stream::{Stream, TryStream}, stream::{Stream, TryStream},
}; };
use crate::{Error, Result};
pub trait IterStream<I: IntoIterator + Send> { pub trait IterStream<I: IntoIterator + Send> {
/// Convert an Iterator into a Stream /// Convert an Iterator into a Stream
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send; fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send;
/// Convert an Iterator into a TryStream /// Convert an Iterator into a TryStream with a generic error type
fn try_stream( fn try_stream<E>(
self, self,
) -> impl TryStream< ) -> impl TryStream<
Ok = <I as IntoIterator>::Item, Ok = <I as IntoIterator>::Item,
Error = Error, Error = E,
Item = Result<<I as IntoIterator>::Item, Error>, Item = Result<<I as IntoIterator>::Item, E>,
> + Send; > + Send;
} }
@@ -28,12 +26,12 @@ where
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send { stream::iter(self) } fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send { stream::iter(self) }
#[inline] #[inline]
fn try_stream( fn try_stream<E>(
self, self,
) -> impl TryStream< ) -> impl TryStream<
Ok = <I as IntoIterator>::Item, Ok = <I as IntoIterator>::Item,
Error = Error, Error = E,
Item = Result<<I as IntoIterator>::Item, Error>, Item = Result<<I as IntoIterator>::Item, E>,
> + Send { > + Send {
self.stream().map(Ok) self.stream().map(Ok)
} }
+2 -1
View File
@@ -1,9 +1,10 @@
//! Synchronous combinator extensions to futures::TryStream //! Synchronous combinator extensions to futures::TryStream
use std::result::Result;
use futures::{TryFuture, TryStream, TryStreamExt}; use futures::{TryFuture, TryStream, TryStreamExt};
use super::automatic_width; use super::automatic_width;
use crate::Result;
/// Concurrency extensions to augment futures::TryStreamExt. broad_ combinators /// Concurrency extensions to augment futures::TryStreamExt. broad_ combinators
/// produce out-of-order /// produce out-of-order
+1 -5
View File
@@ -2,8 +2,6 @@
use std::{cell::Cell, fmt::Debug, path::PathBuf, sync::LazyLock}; use std::{cell::Cell, fmt::Debug, path::PathBuf, sync::LazyLock};
use snafu::IntoError;
use crate::{Result, is_equal_to}; use crate::{Result, is_equal_to};
type Id = usize; type Id = usize;
@@ -144,9 +142,7 @@ pub fn getcpu() -> Result<usize> {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
#[inline] #[inline]
pub fn getcpu() -> Result<usize> { pub fn getcpu() -> Result<usize> { Err(crate::Error::Io(std::io::ErrorKind::Unsupported.into())) }
Err(crate::error::IoSnafu.into_error(std::io::ErrorKind::Unsupported.into()))
}
fn query_cores_available() -> impl Iterator<Item = Id> { fn query_cores_available() -> impl Iterator<Item = Id> {
core_affinity::get_core_ids() core_affinity::get_core_ids()
+7 -12
View File
@@ -255,10 +255,7 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
| "$serde_json::private::RawValue" => visitor.visit_map(self), | "$serde_json::private::RawValue" => visitor.visit_map(self),
| "Cbor" => visitor | "Cbor" => visitor
.visit_newtype_struct(&mut minicbor_serde::Deserializer::new(self.record_trail())) .visit_newtype_struct(&mut minicbor_serde::Deserializer::new(self.record_trail()))
.map_err(|e| { .map_err(|e| Self::Error::SerdeDe(e.to_string().into())),
let message: std::borrow::Cow<'static, str> = e.to_string().into();
conduwuit_core::error::SerdeDeSnafu { message }.build()
}),
| _ => visitor.visit_newtype_struct(self), | _ => visitor.visit_newtype_struct(self),
} }
@@ -316,10 +313,9 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
let end = self.pos.saturating_add(BYTES).min(self.buf.len()); let end = self.pos.saturating_add(BYTES).min(self.buf.len());
let bytes: ArrayVec<u8, BYTES> = self.buf[self.pos..end].try_into()?; let bytes: ArrayVec<u8, BYTES> = self.buf[self.pos..end].try_into()?;
let bytes = bytes.into_inner().map_err(|_| { let bytes = bytes
let message: std::borrow::Cow<'static, str> = "i64 buffer underflow".into(); .into_inner()
conduwuit_core::error::SerdeDeSnafu { message }.build() .map_err(|_| Self::Error::SerdeDe("i64 buffer underflow".into()))?;
})?;
self.inc_pos(BYTES); self.inc_pos(BYTES);
visitor.visit_i64(i64::from_be_bytes(bytes)) visitor.visit_i64(i64::from_be_bytes(bytes))
@@ -349,10 +345,9 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
let end = self.pos.saturating_add(BYTES).min(self.buf.len()); let end = self.pos.saturating_add(BYTES).min(self.buf.len());
let bytes: ArrayVec<u8, BYTES> = self.buf[self.pos..end].try_into()?; let bytes: ArrayVec<u8, BYTES> = self.buf[self.pos..end].try_into()?;
let bytes = bytes.into_inner().map_err(|_| { let bytes = bytes
let message: std::borrow::Cow<'static, str> = "u64 buffer underflow".into(); .into_inner()
conduwuit_core::error::SerdeDeSnafu { message }.build() .map_err(|_| Self::Error::SerdeDe("u64 buffer underflow".into()))?;
})?;
self.inc_pos(BYTES); self.inc_pos(BYTES);
visitor.visit_u64(u64::from_be_bytes(bytes)) visitor.visit_u64(u64::from_be_bytes(bytes))
+1 -4
View File
@@ -199,10 +199,7 @@ impl<W: Write> ser::Serializer for &mut Serializer<'_, W> {
value value
.serialize(&mut Serializer::new(&mut Writer::new(&mut self.out))) .serialize(&mut Serializer::new(&mut Writer::new(&mut self.out)))
.map_err(|e| { .map_err(|e| Self::Error::SerdeSer(e.to_string().into()))
let message: std::borrow::Cow<'static, str> = e.to_string().into();
conduwuit_core::error::SerdeSerSnafu { message }.build()
})
}, },
| _ => unhandled!("Unrecognized serialization Newtype {name:?}"), | _ => unhandled!("Unrecognized serialization Newtype {name:?}"),
} }
+2 -7
View File
@@ -1,4 +1,4 @@
use std::{borrow::Cow, sync::Arc}; use std::sync::Arc;
use axum::{Router, response::IntoResponse}; use axum::{Router, response::IntoResponse};
use conduwuit::Error; use conduwuit::Error;
@@ -18,10 +18,5 @@ pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
} }
async fn not_found(_uri: Uri) -> impl IntoResponse { async fn not_found(_uri: Uri) -> impl IntoResponse {
Error::Request { Error::Request(ErrorKind::Unrecognized, "Not Found".into(), StatusCode::NOT_FOUND)
kind: ErrorKind::Unrecognized,
message: Cow::Borrowed("Not Found"),
code: StatusCode::NOT_FOUND,
backtrace: None,
}
} }
+4 -4
View File
@@ -147,11 +147,11 @@ impl Service {
// same appservice) // same appservice)
if let Ok(existing) = self.find_from_token(&registration.as_token).await { if let Ok(existing) = self.find_from_token(&registration.as_token).await {
if existing.registration.id != registration.id { if existing.registration.id != registration.id {
return Err!(Request(InvalidParam( return Err(err!(Request(InvalidParam(
"Cannot register appservice: Token is already used by appservice '{}'. \ "Cannot register appservice: Token is already used by appservice '{}'. \
Please generate a different token.", Please generate a different token.",
existing.registration.id existing.registration.id
))); ))));
} }
} }
@@ -163,10 +163,10 @@ impl Service {
.await .await
.is_ok() .is_ok()
{ {
return Err!(Request(InvalidParam( return Err(err!(Request(InvalidParam(
"Cannot register appservice: The provided token is already in use by a user \ "Cannot register appservice: The provided token is already in use by a user \
device. Please generate a different token for the appservice." device. Please generate a different token for the appservice."
))); ))));
} }
self.db self.db
+1 -1
View File
@@ -39,7 +39,7 @@ impl crate::Service for Service {
let url_preview_user_agent = config let url_preview_user_agent = config
.url_preview_user_agent .url_preview_user_agent
.clone() .clone()
.unwrap_or_else(|| conduwuit::version::user_agent().to_owned()); .unwrap_or_else(|| conduwuit::version::user_agent_media().to_owned());
Ok(Arc::new(Self { Ok(Arc::new(Self {
default: base(config)? default: base(config)?
+5 -2
View File
@@ -2,7 +2,7 @@ use std::{fmt::Debug, mem};
use bytes::Bytes; use bytes::Bytes;
use conduwuit::{ use conduwuit::{
Err, Result, debug, debug::INFO_SPAN_LEVEL, debug_error, debug_warn, err, Err, Error, Result, debug, debug::INFO_SPAN_LEVEL, debug_error, debug_warn, err,
error::inspect_debug_log, implement, trace, error::inspect_debug_log, implement, trace,
}; };
use http::{HeaderValue, header::AUTHORIZATION}; use http::{HeaderValue, header::AUTHORIZATION};
@@ -179,7 +179,10 @@ async fn into_http_response(
debug!("Got {status:?} for {method} {url}"); debug!("Got {status:?} for {method} {url}");
if !status.is_success() { if !status.is_success() {
return Err!(Federation(dest.to_owned(), RumaError::from_http_response(http_response),)); return Err(Error::Federation(
dest.to_owned(),
RumaError::from_http_response(http_response),
));
} }
Ok(http_response) Ok(http_response)
+11 -9
View File
@@ -67,15 +67,17 @@ impl crate::Service for Service {
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
async fn worker(self: Arc<Self>) -> Result { async fn worker(self: Arc<Self>) -> Result {
// first run mode will be enabled if there are no local users // first run mode will be enabled if there are no local users, provided it's not
let is_first_run = self // forcibly disabled for Complement tests
.services let is_first_run = !self.services.config.force_disable_first_run_mode
.users && self
.list_local_users() .services
.ready_filter(|user| *user != self.services.globals.server_user) .users
.next() .list_local_users()
.await .ready_filter(|user| *user != self.services.globals.server_user)
.is_none(); .next()
.await
.is_none();
self.first_run_marker self.first_run_marker
.set(if is_first_run { .set(if is_first_run {
+2
View File
@@ -170,6 +170,8 @@ impl Data {
Ok(()) Ok(())
} }
pub(super) async fn clear_url_previews(&self) { self.url_previews.clear().await; }
pub(super) fn set_url_preview( pub(super) fn set_url_preview(
&self, &self,
url: &str, url: &str,
+3
View File
@@ -37,6 +37,9 @@ pub async fn remove_url_preview(&self, url: &str) -> Result<()> {
self.db.remove_url_preview(url) self.db.remove_url_preview(url)
} }
#[implement(Service)]
pub async fn clear_url_previews(&self) { self.db.clear_url_previews().await; }
#[implement(Service)] #[implement(Service)]
pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> { pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> {
let now = SystemTime::now() let now = SystemTime::now()
+2 -2
View File
@@ -35,7 +35,7 @@ pub async fn fetch_remote_thumbnail(
.fetch_thumbnail_authenticated(mxc, user, server, timeout_ms, dim) .fetch_thumbnail_authenticated(mxc, user, server, timeout_ms, dim)
.await; .await;
if let Err(Error::Request { kind: NotFound, .. }) = &result { if let Err(Error::Request(NotFound, ..)) = &result {
return self return self
.fetch_thumbnail_unauthenticated(mxc, user, server, timeout_ms, dim) .fetch_thumbnail_unauthenticated(mxc, user, server, timeout_ms, dim)
.await; .await;
@@ -67,7 +67,7 @@ pub async fn fetch_remote_content(
); );
}); });
if let Err(Error::Request { kind: Unrecognized, .. }) = &result { if let Err(Error::Request(Unrecognized, ..)) = &result {
return self return self
.fetch_content_unauthenticated(mxc, user, server, timeout_ms) .fetch_content_unauthenticated(mxc, user, server, timeout_ms)
.await; .await;
+1 -1
View File
@@ -31,7 +31,7 @@ pub mod rooms;
pub mod sending; pub mod sending;
pub mod server_keys; pub mod server_keys;
pub mod sync; pub mod sync;
pub mod transaction_ids; pub mod transactions;
pub mod uiaa; pub mod uiaa;
pub mod users; pub mod users;
+1 -1
View File
@@ -142,7 +142,7 @@ async fn get_auth_chain_outer(
let chunk_cache: Vec<_> = chunk let chunk_cache: Vec<_> = chunk
.into_iter() .into_iter()
.try_stream() .try_stream::<conduwuit::Error>()
.broad_and_then(|(shortid, event_id)| async move { .broad_and_then(|(shortid, event_id)| async move {
if let Ok(cached) = self.get_cached_eventid_authchain(&[shortid]).await { if let Ok(cached) = self.get_cached_eventid_authchain(&[shortid]).await {
return Ok(cached.to_vec()); return Ok(cached.to_vec());
@@ -63,7 +63,9 @@ where
}, },
| hash_map::Entry::Occupied(_) => { | hash_map::Entry::Occupied(_) => {
return Err!(Database( return Err!(Database(
"State event's type and state_key combination exists multiple times.", "State event's type and state_key combination exists multiple times: {}, {}",
pdu.kind(),
state_key
)); ));
}, },
} }
@@ -162,7 +162,9 @@ where
}, },
| hash_map::Entry::Occupied(_) => { | hash_map::Entry::Occupied(_) => {
return Err!(Request(InvalidParam( return Err!(Request(InvalidParam(
"Auth event's type and state_key combination exists multiple times.", "Auth event's type and state_key combination exists multiple times: {}, {}",
auth_event.kind,
auth_event.state_key().unwrap_or("")
))); )));
}, },
} }
@@ -112,14 +112,7 @@ where
{ {
let event_fetch = |event_id| self.event_fetch(event_id); let event_fetch = |event_id| self.event_fetch(event_id);
let event_exists = |event_id| self.event_exists(event_id); let event_exists = |event_id| self.event_exists(event_id);
Ok( state_res::resolve(room_version, state_sets, auth_chain_sets, &event_fetch, &event_exists)
state_res::resolve( .map_err(|e| err!(error!("State resolution failed: {e:?}")))
room_version, .await
state_sets,
auth_chain_sets,
&event_fetch,
&event_exists,
)
.await?,
)
} }
+2 -2
View File
@@ -3,7 +3,7 @@ use std::{
str::FromStr, str::FromStr,
}; };
use conduwuit::{Err, Error, Result}; use conduwuit::{Error, Result};
use ruma::{UInt, api::client::error::ErrorKind}; use ruma::{UInt, api::client::error::ErrorKind};
use crate::rooms::short::ShortRoomId; use crate::rooms::short::ShortRoomId;
@@ -57,7 +57,7 @@ impl FromStr for PaginationToken {
if let Some(token) = pag_tok() { if let Some(token) = pag_tok() {
Ok(token) Ok(token)
} else { } else {
Err!(BadRequest(ErrorKind::InvalidParam, "invalid token")) Err(Error::BadRequest(ErrorKind::InvalidParam, "invalid token"))
} }
} }
} }
+5 -4
View File
@@ -75,7 +75,10 @@ pub async fn create_hash_and_sign_event(
let content: RoomCreateEventContent = serde_json::from_str(content.get())?; let content: RoomCreateEventContent = serde_json::from_str(content.get())?;
Ok(content.room_version) Ok(content.room_version)
} else { } else {
Err!(InconsistentRoomState("non-create event for room of unknown version", room_id)) Err(Error::InconsistentRoomState(
"non-create event for room of unknown version",
room_id,
))
} }
} }
let PduBuilder { let PduBuilder {
@@ -272,9 +275,7 @@ pub async fn create_hash_and_sign_event(
.hash_and_sign_event(&mut pdu_json, &room_version_id) .hash_and_sign_event(&mut pdu_json, &room_version_id)
{ {
return match e { return match e {
| Error::Signatures { source, .. } | Error::Signatures(ruma::signatures::Error::PduSize) => {
if matches!(source, ruma::signatures::Error::PduSize) =>
{
Err!(Request(TooLarge("Message/PDU is too long (exceeds 65535 bytes)"))) Err!(Request(TooLarge("Message/PDU is too long (exceeds 65535 bytes)")))
}, },
| _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))), | _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))),
+3 -3
View File
@@ -14,7 +14,7 @@ use crate::{
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending, media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
server_keys, server_keys,
service::{self, Args, Map, Service}, service::{self, Args, Map, Service},
sync, transaction_ids, uiaa, users, sync, transactions, uiaa, users,
}; };
pub struct Services { pub struct Services {
@@ -37,7 +37,7 @@ pub struct Services {
pub sending: Arc<sending::Service>, pub sending: Arc<sending::Service>,
pub server_keys: Arc<server_keys::Service>, pub server_keys: Arc<server_keys::Service>,
pub sync: Arc<sync::Service>, pub sync: Arc<sync::Service>,
pub transaction_ids: Arc<transaction_ids::Service>, pub transactions: Arc<transactions::Service>,
pub uiaa: Arc<uiaa::Service>, pub uiaa: Arc<uiaa::Service>,
pub users: Arc<users::Service>, pub users: Arc<users::Service>,
pub moderation: Arc<moderation::Service>, pub moderation: Arc<moderation::Service>,
@@ -110,7 +110,7 @@ impl Services {
sending: build!(sending::Service), sending: build!(sending::Service),
server_keys: build!(server_keys::Service), server_keys: build!(server_keys::Service),
sync: build!(sync::Service), sync: build!(sync::Service),
transaction_ids: build!(transaction_ids::Service), transactions: build!(transactions::Service),
uiaa: build!(uiaa::Service), uiaa: build!(uiaa::Service),
users: build!(users::Service), users: build!(users::Service),
moderation: build!(moderation::Service), moderation: build!(moderation::Service),
-54
View File
@@ -1,54 +0,0 @@
use std::sync::Arc;
use conduwuit::{Result, implement};
use database::{Handle, Map};
use ruma::{DeviceId, TransactionId, UserId};
pub struct Service {
db: Data,
}
struct Data {
userdevicetxnid_response: Arc<Map>,
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
db: Data {
userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
#[implement(Service)]
pub fn add_txnid(
&self,
user_id: &UserId,
device_id: Option<&DeviceId>,
txn_id: &TransactionId,
data: &[u8],
) {
let mut key = user_id.as_bytes().to_vec();
key.push(0xFF);
key.extend_from_slice(device_id.map(DeviceId::as_bytes).unwrap_or_default());
key.push(0xFF);
key.extend_from_slice(txn_id.as_bytes());
self.db.userdevicetxnid_response.insert(&key, data);
}
// If there's no entry, this is a new transaction
#[implement(Service)]
pub async fn existing_txnid(
&self,
user_id: &UserId,
device_id: Option<&DeviceId>,
txn_id: &TransactionId,
) -> Result<Handle<'_>> {
let key = (user_id, device_id, txn_id);
self.db.userdevicetxnid_response.qry(&key).await
}
+326
View File
@@ -0,0 +1,326 @@
use std::{
collections::HashMap,
fmt,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{Duration, SystemTime},
};
use async_trait::async_trait;
use conduwuit::{Error, Result, SyncRwLock, debug_warn, warn};
use database::{Handle, Map};
use ruma::{
DeviceId, OwnedServerName, OwnedTransactionId, TransactionId, UserId,
api::{
client::error::ErrorKind::LimitExceeded,
federation::transactions::send_transaction_message,
},
};
use tokio::sync::watch::{Receiver, Sender};
use crate::{Dep, config};
pub type TxnKey = (OwnedServerName, OwnedTransactionId);
pub type WrappedTransactionResponse =
Option<Result<send_transaction_message::v1::Response, TransactionError>>;
/// Errors that can occur during federation transaction processing.
#[derive(Debug, Clone)]
pub enum TransactionError {
/// Server is shutting down - the sender should retry the entire
/// transaction.
ShuttingDown,
}
impl fmt::Display for TransactionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
| Self::ShuttingDown => write!(f, "Server is shutting down"),
}
}
}
impl std::error::Error for TransactionError {}
/// Minimum interval between cache cleanup runs.
/// Exists to prevent thrashing when the cache is full of things that can't be
/// cleared
const CLEANUP_INTERVAL_SECS: u64 = 30;
#[derive(Clone, Debug)]
pub struct CachedTxnResponse {
pub response: send_transaction_message::v1::Response,
pub created: SystemTime,
}
/// Internal state for a federation transaction.
/// Either actively being processed or completed and cached.
#[derive(Clone)]
enum TxnState {
/// Transaction is currently being processed.
Active(Receiver<WrappedTransactionResponse>),
/// Transaction completed and response is cached.
Cached(CachedTxnResponse),
}
/// Result of atomically checking or starting a federation transaction.
pub enum FederationTxnState {
/// Transaction already completed and cached
Cached(send_transaction_message::v1::Response),
/// Transaction is currently being processed by another request.
/// Wait on this receiver for the result.
Active(Receiver<WrappedTransactionResponse>),
/// This caller should process the transaction (first to request it).
Started {
receiver: Receiver<WrappedTransactionResponse>,
sender: Sender<WrappedTransactionResponse>,
},
}
pub struct Service {
services: Services,
db: Data,
federation_txn_state: Arc<SyncRwLock<HashMap<TxnKey, TxnState>>>,
last_cleanup: AtomicU64,
}
struct Services {
config: Dep<config::Service>,
}
struct Data {
userdevicetxnid_response: Arc<Map>,
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
config: args.depend::<config::Service>("config"),
},
db: Data {
userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
},
federation_txn_state: Arc::new(SyncRwLock::new(HashMap::new())),
last_cleanup: AtomicU64::new(0),
}))
}
async fn clear_cache(&self) {
let mut state = self.federation_txn_state.write();
// Only clear cached entries, preserve active transactions
state.retain(|_, v| matches!(v, TxnState::Active(_)));
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Returns the count of currently active (in-progress) transactions.
#[must_use]
pub fn txn_active_handle_count(&self) -> usize {
let state = self.federation_txn_state.read();
state
.values()
.filter(|v| matches!(v, TxnState::Active(_)))
.count()
}
pub fn add_client_txnid(
&self,
user_id: &UserId,
device_id: Option<&DeviceId>,
txn_id: &TransactionId,
data: &[u8],
) {
let mut key = user_id.as_bytes().to_vec();
key.push(0xFF);
key.extend_from_slice(device_id.map(DeviceId::as_bytes).unwrap_or_default());
key.push(0xFF);
key.extend_from_slice(txn_id.as_bytes());
self.db.userdevicetxnid_response.insert(&key, data);
}
pub async fn get_client_txn(
&self,
user_id: &UserId,
device_id: Option<&DeviceId>,
txn_id: &TransactionId,
) -> Result<Handle<'_>> {
let key = (user_id, device_id, txn_id);
self.db.userdevicetxnid_response.qry(&key).await
}
/// Atomically gets a cached response, joins an active transaction, or
/// starts a new one.
pub fn get_or_start_federation_txn(&self, key: TxnKey) -> Result<FederationTxnState> {
// Only one upgradable lock can be held at a time, and there aren't any
// read-only locks, so no point being upgradable
let mut state = self.federation_txn_state.write();
// Check existing state for this key
if let Some(txn_state) = state.get(&key) {
return Ok(match txn_state {
| TxnState::Cached(cached) => FederationTxnState::Cached(cached.response.clone()),
| TxnState::Active(receiver) => FederationTxnState::Active(receiver.clone()),
});
}
// Check if another transaction from this origin is already running
let has_active_from_origin = state
.iter()
.any(|(k, v)| k.0 == key.0 && matches!(v, TxnState::Active(_)));
if has_active_from_origin {
debug_warn!(
origin = ?key.0,
"Got concurrent transaction request from an origin with an active transaction"
);
return Err(Error::BadRequest(
LimitExceeded { retry_after: None },
"Still processing another transaction from this origin",
));
}
let max_active_txns = self.services.config.max_concurrent_inbound_transactions;
// Check if we're at capacity
if state.len() >= max_active_txns
&& let active_count = state
.values()
.filter(|v| matches!(v, TxnState::Active(_)))
.count() && active_count >= max_active_txns
{
warn!(
active = active_count,
max = max_active_txns,
"Server is overloaded, dropping incoming transaction"
);
return Err(Error::BadRequest(
LimitExceeded { retry_after: None },
"Server is overloaded, try again later",
));
}
// Start new transaction
let (sender, receiver) = tokio::sync::watch::channel(None);
state.insert(key, TxnState::Active(receiver.clone()));
Ok(FederationTxnState::Started { receiver, sender })
}
/// Finishes a transaction by transitioning it from active to cached state.
/// Additionally may trigger cleanup of old entries.
pub fn finish_federation_txn(
&self,
key: TxnKey,
sender: Sender<WrappedTransactionResponse>,
response: send_transaction_message::v1::Response,
) {
// Check if cleanup might be needed before acquiring the lock
let should_try_cleanup = self.should_try_cleanup();
let mut state = self.federation_txn_state.write();
// Explicitly set cached first so there is no gap where receivers get a closed
// channel
state.insert(
key,
TxnState::Cached(CachedTxnResponse {
response: response.clone(),
created: SystemTime::now(),
}),
);
if let Err(e) = sender.send(Some(Ok(response))) {
debug_warn!("Failed to send transaction response to waiting receivers: {e}");
}
// Explicitly close
drop(sender);
// This task is dangling, we can try clean caches now
if should_try_cleanup {
self.cleanup_entries_locked(&mut state);
}
}
pub fn remove_federation_txn(&self, key: &TxnKey) {
let mut state = self.federation_txn_state.write();
state.remove(key);
}
/// Checks if enough time has passed since the last cleanup to consider
/// running another. Updates the last cleanup time if returning true.
fn should_try_cleanup(&self) -> bool {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime before UNIX_EPOCH")
.as_secs();
let last = self.last_cleanup.load(Ordering::Relaxed);
if now.saturating_sub(last) >= CLEANUP_INTERVAL_SECS {
// CAS: only update if no one else has updated it since we read
self.last_cleanup
.compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
} else {
false
}
}
/// Cleans up cached entries based on age and count limits.
///
/// First removes all cached entries older than the configured max age.
/// Then, if the cache still exceeds the max entry count, removes the oldest
/// cached entries until the count is within limits.
///
/// Must be called with write lock held on the state map.
fn cleanup_entries_locked(&self, state: &mut HashMap<TxnKey, TxnState>) {
let max_age_secs = self.services.config.transaction_id_cache_max_age_secs;
let max_entries = self.services.config.transaction_id_cache_max_entries;
// First pass: remove all cached entries older than max age
let cutoff = SystemTime::now()
.checked_sub(Duration::from_secs(max_age_secs))
.unwrap_or(SystemTime::UNIX_EPOCH);
state.retain(|_, v| match v {
| TxnState::Active(_) => true, // Never remove active transactions
| TxnState::Cached(cached) => cached.created > cutoff,
});
// Count cached entries
let cached_count = state
.values()
.filter(|v| matches!(v, TxnState::Cached(_)))
.count();
// Second pass: if still over max entries, remove oldest cached entries
if cached_count > max_entries {
let excess = cached_count.saturating_sub(max_entries);
// Collect cached entries sorted by age (oldest first)
let mut cached_entries: Vec<_> = state
.iter()
.filter_map(|(k, v)| match v {
| TxnState::Cached(cached) => Some((k.clone(), cached.created)),
| TxnState::Active(_) => None,
})
.collect();
cached_entries.sort_by(|a, b| a.1.cmp(&b.1));
// Remove the oldest cached entries to get under the limit
for (key, _) in cached_entries.into_iter().take(excess) {
state.remove(&key);
}
}
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
use conduwuit::{ use conduwuit::{
Err, Result, SyncRwLock, err, error, implement, utils, Err, Error, Result, SyncRwLock, err, error, implement, utils,
utils::{hash, string::EMPTY}, utils::{hash, string::EMPTY},
}; };
use database::{Deserialized, Json, Map}; use database::{Deserialized, Json, Map};
@@ -117,7 +117,7 @@ pub async fn try_auth(
} else if let Some(username) = user { } else if let Some(username) = user {
username username
} else { } else {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::Unrecognized, ErrorKind::Unrecognized,
"Identifier type not recognized.", "Identifier type not recognized.",
)); ));
@@ -125,7 +125,7 @@ pub async fn try_auth(
#[cfg(not(feature = "element_hacks"))] #[cfg(not(feature = "element_hacks"))]
let Some(UserIdentifier::UserIdOrLocalpart(username)) = identifier else { let Some(UserIdentifier::UserIdOrLocalpart(username)) = identifier else {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::Unrecognized, ErrorKind::Unrecognized,
"Identifier type not recognized.", "Identifier type not recognized.",
)); ));
@@ -135,7 +135,7 @@ pub async fn try_auth(
username.clone(), username.clone(),
self.services.globals.server_name(), self.services.globals.server_name(),
) )
.map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "User ID is invalid.")))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "User ID is invalid."))?;
// Check if the access token being used matches the credentials used for UIAA // Check if the access token being used matches the credentials used for UIAA
if user_id.localpart() != user_id_from_username.localpart() { if user_id.localpart() != user_id_from_username.localpart() {
+6 -6
View File
@@ -761,13 +761,13 @@ impl Service {
.keys .keys
.into_values(); .into_values();
let self_signing_key_id = self_signing_key_ids.next().ok_or(err!(BadRequest( let self_signing_key_id = self_signing_key_ids.next().ok_or(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Self signing key contained no key.", "Self signing key contained no key.",
)))?; ))?;
if self_signing_key_ids.next().is_some() { if self_signing_key_ids.next().is_some() {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Self signing key contained more than one key.", "Self signing key contained more than one key.",
)); ));
@@ -1439,13 +1439,13 @@ pub fn parse_master_key(
let master_key = master_key let master_key = master_key
.deserialize() .deserialize()
.map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "Invalid master key")))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid master key"))?;
let mut master_key_ids = master_key.keys.values(); let mut master_key_ids = master_key.keys.values();
let master_key_id = master_key_ids let master_key_id = master_key_ids
.next() .next()
.ok_or(err!(BadRequest(ErrorKind::InvalidParam, "Master key contained no key.")))?; .ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Master key contained no key."))?;
if master_key_ids.next().is_some() { if master_key_ids.next().is_some() {
return Err!(BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Master key contained more than one key.", "Master key contained more than one key.",
)); ));
+1 -1
View File
@@ -25,7 +25,7 @@ axum.workspace = true
futures.workspace = true futures.workspace = true
tracing.workspace = true tracing.workspace = true
rand.workspace = true rand.workspace = true
snafu.workspace = true thiserror.workspace = true
[lints] [lints]
workspace = true workspace = true
+4 -12
View File
@@ -8,7 +8,6 @@ use axum::{
}; };
use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, version_tag}; use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, version_tag};
use conduwuit_service::state; use conduwuit_service::state;
use snafu::{IntoError, prelude::*};
pub fn build() -> Router<state::State> { pub fn build() -> Router<state::State> {
Router::<state::State>::new() Router::<state::State>::new()
@@ -49,17 +48,10 @@ async fn logo_handler() -> impl IntoResponse {
) )
} }
#[derive(Debug, Snafu)] #[derive(Debug, thiserror::Error)]
enum WebError { enum WebError {
#[snafu(display("Failed to render template: {source}"))] #[error("Failed to render template: {0}")]
Render { Render(#[from] askama::Error),
source: askama::Error,
backtrace: snafu::Backtrace,
},
}
impl From<askama::Error> for WebError {
fn from(source: askama::Error) -> Self { RenderSnafu.into_error(source) }
} }
impl IntoResponse for WebError { impl IntoResponse for WebError {
@@ -74,7 +66,7 @@ impl IntoResponse for WebError {
let nonce = rand::random::<u64>().to_string(); let nonce = rand::random::<u64>().to_string();
let status = match &self { let status = match &self {
| Self::Render { .. } => StatusCode::INTERNAL_SERVER_ERROR, | Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
}; };
let tmpl = Error { nonce: &nonce, err: self }; let tmpl = Error { nonce: &nonce, err: self };
if let Ok(body) = tmpl.render() { if let Ok(body) = tmpl.render() {
+2 -2
View File
@@ -12,8 +12,8 @@ Server Error
</h1> </h1>
{%- match err -%} {%- match err -%}
{% when WebError::Render { source, .. } -%} {% when WebError::Render(err) -%}
<pre>{{ source }}</pre> <pre>{{ err }}</pre>
{% else -%} <p>An error occurred</p> {% else -%} <p>An error occurred</p>
{%- endmatch -%} {%- endmatch -%}
+125 -37
View File
@@ -6,9 +6,9 @@
{"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_has_events"} {"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_has_events"}
{"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_has_events/incremental_sync"} {"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_has_events/incremental_sync"}
{"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_has_events/initial_sync"} {"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_has_events/initial_sync"}
{"Action":"pass","Test":"TestArchivedRoomsHistory/timeline_is_empty"} {"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_is_empty"}
{"Action":"skip","Test":"TestArchivedRoomsHistory/timeline_is_empty/incremental_sync"} {"Action":"skip","Test":"TestArchivedRoomsHistory/timeline_is_empty/incremental_sync"}
{"Action":"pass","Test":"TestArchivedRoomsHistory/timeline_is_empty/initial_sync"} {"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_is_empty/initial_sync"}
{"Action":"fail","Test":"TestAsyncUpload"} {"Action":"fail","Test":"TestAsyncUpload"}
{"Action":"fail","Test":"TestAsyncUpload/Cannot_upload_to_a_media_ID_that_has_already_been_uploaded_to"} {"Action":"fail","Test":"TestAsyncUpload/Cannot_upload_to_a_media_ID_that_has_already_been_uploaded_to"}
{"Action":"fail","Test":"TestAsyncUpload/Create_media"} {"Action":"fail","Test":"TestAsyncUpload/Create_media"}
@@ -79,9 +79,11 @@
{"Action":"fail","Test":"TestClientSpacesSummary/redact_link"} {"Action":"fail","Test":"TestClientSpacesSummary/redact_link"}
{"Action":"fail","Test":"TestClientSpacesSummary/suggested_only"} {"Action":"fail","Test":"TestClientSpacesSummary/suggested_only"}
{"Action":"fail","Test":"TestClientSpacesSummaryJoinRules"} {"Action":"fail","Test":"TestClientSpacesSummaryJoinRules"}
{"Action":"pass","Test":"TestComplementCanCreateValidV12Rooms"}
{"Action":"pass","Test":"TestContent"} {"Action":"pass","Test":"TestContent"}
{"Action":"pass","Test":"TestContentCSAPIMediaV1"} {"Action":"pass","Test":"TestContentCSAPIMediaV1"}
{"Action":"pass","Test":"TestContentMediaV1"} {"Action":"pass","Test":"TestContentMediaV1"}
{"Action":"fail","Test":"TestCorruptedAuthChain"}
{"Action":"pass","Test":"TestCumulativeJoinLeaveJoinSync"} {"Action":"pass","Test":"TestCumulativeJoinLeaveJoinSync"}
{"Action":"pass","Test":"TestDeactivateAccount"} {"Action":"pass","Test":"TestDeactivateAccount"}
{"Action":"pass","Test":"TestDeactivateAccount/After_deactivating_account,_can't_log_in_with_password"} {"Action":"pass","Test":"TestDeactivateAccount/After_deactivating_account,_can't_log_in_with_password"}
@@ -89,19 +91,18 @@
{"Action":"pass","Test":"TestDeactivateAccount/Can_deactivate_account"} {"Action":"pass","Test":"TestDeactivateAccount/Can_deactivate_account"}
{"Action":"pass","Test":"TestDeactivateAccount/Password_flow_is_available"} {"Action":"pass","Test":"TestDeactivateAccount/Password_flow_is_available"}
{"Action":"fail","Test":"TestDelayedEvents"} {"Action":"fail","Test":"TestDelayedEvents"}
{"Action":"fail","Test":"TestDelayedEvents/cannot_update_a_delayed_event_with_an_invalid_action"} {"Action":"pass","Test":"TestDelayedEvents/cannot_update_a_delayed_event_with_an_invalid_action"}
{"Action":"pass","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_a_delay_ID"} {"Action":"pass","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_an_action"}
{"Action":"fail","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_a_request_body"} {"Action":"fail","Test":"TestDelayedEvents/delayed_event_lookups_are_authenticated"}
{"Action":"fail","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_an_action"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_events_are_empty_on_startup"} {"Action":"fail","Test":"TestDelayedEvents/delayed_events_are_empty_on_startup"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_message_events_are_sent_on_timeout"} {"Action":"fail","Test":"TestDelayedEvents/delayed_message_events_are_sent_on_timeout"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_are_cancelled_by_a_more_recent_state_event_from_another_user"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_are_cancelled_by_a_more_recent_state_event_from_the_same_user"}
{"Action":"skip","Test":"TestDelayedEvents/delayed_state_events_are_kept_on_server_restart"} {"Action":"skip","Test":"TestDelayedEvents/delayed_state_events_are_kept_on_server_restart"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_are_sent_on_timeout"} {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_are_sent_on_timeout"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_cancelled"} {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_cancelled"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_restarted"} {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_restarted"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_sent_on_request"} {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_sent_on_request"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_is_cancelled_by_new_state_from_another_user"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_state_is_not_cancelled_by_new_state_from_the_same_user"}
{"Action":"pass","Test":"TestDelayedEvents/parallel"} {"Action":"pass","Test":"TestDelayedEvents/parallel"}
{"Action":"pass","Test":"TestDelayedEvents/parallel/cannot_cancel_a_delayed_event_without_a_matching_delay_ID"} {"Action":"pass","Test":"TestDelayedEvents/parallel/cannot_cancel_a_delayed_event_without_a_matching_delay_ID"}
{"Action":"pass","Test":"TestDelayedEvents/parallel/cannot_restart_a_delayed_event_without_a_matching_delay_ID"} {"Action":"pass","Test":"TestDelayedEvents/parallel/cannot_restart_a_delayed_event_without_a_matching_delay_ID"}
@@ -153,12 +154,14 @@
{"Action":"fail","Test":"TestFederationKeyUploadQuery/Can_query_remote_device_keys_using_POST"} {"Action":"fail","Test":"TestFederationKeyUploadQuery/Can_query_remote_device_keys_using_POST"}
{"Action":"pass","Test":"TestFederationRedactSendsWithoutEvent"} {"Action":"pass","Test":"TestFederationRedactSendsWithoutEvent"}
{"Action":"pass","Test":"TestFederationRejectInvite"} {"Action":"pass","Test":"TestFederationRejectInvite"}
{"Action":"pass","Test":"TestFederationRoomsInvite"} {"Action":"fail","Test":"TestFederationRoomsInvite"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel"} {"Action":"fail","Test":"TestFederationRoomsInvite/Parallel"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_can_reject_invite_over_federation"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_can_reject_invite_over_federation"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_can_reject_invite_over_federation_for_empty_room"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_can_reject_invite_over_federation_for_empty_room"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_can_reject_invite_over_federation_several_times"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_can_reject_invite_over_federation_several_times"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_has_'is_direct'_flag_in_prev_content_after_joining"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Invited_user_has_'is_direct'_flag_in_prev_content_after_joining"}
{"Action":"fail","Test":"TestFederationRoomsInvite/Parallel/Inviter_user_can_rescind_invite_over_federation"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Non-invitee_user_cannot_rescind_invite_over_federation"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Remote_invited_user_can_join_the_room_when_homeserver_is_already_participating_in_the_room"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Remote_invited_user_can_join_the_room_when_homeserver_is_already_participating_in_the_room"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Remote_invited_user_can_reject_invite_when_homeserver_is_already_participating_in_the_room"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Remote_invited_user_can_reject_invite_when_homeserver_is_already_participating_in_the_room"}
{"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Remote_invited_user_can_see_room_metadata"} {"Action":"pass","Test":"TestFederationRoomsInvite/Parallel/Remote_invited_user_can_see_room_metadata"}
@@ -191,6 +194,18 @@
{"Action":"pass","Test":"TestInboundFederationProfile/Inbound_federation_can_query_profile_data"} {"Action":"pass","Test":"TestInboundFederationProfile/Inbound_federation_can_query_profile_data"}
{"Action":"pass","Test":"TestInboundFederationProfile/Non-numeric_ports_in_server_names_are_rejected"} {"Action":"pass","Test":"TestInboundFederationProfile/Non-numeric_ports_in_server_names_are_rejected"}
{"Action":"fail","Test":"TestInboundFederationRejectsEventsWithRejectedAuthEvents"} {"Action":"fail","Test":"TestInboundFederationRejectsEventsWithRejectedAuthEvents"}
{"Action":"pass","Test":"TestInviteFiltering"}
{"Action":"pass","Test":"TestInviteFiltering/Can_allow_a_user_from_a_blocked_server"}
{"Action":"pass","Test":"TestInviteFiltering/Can_block_a_single_user"}
{"Action":"pass","Test":"TestInviteFiltering/Can_block_a_user_from_an_allowed_server"}
{"Action":"pass","Test":"TestInviteFiltering/Can_block_a_whole_server"}
{"Action":"pass","Test":"TestInviteFiltering/Can_glob_serveral_servers"}
{"Action":"pass","Test":"TestInviteFiltering/Can_glob_serveral_users"}
{"Action":"pass","Test":"TestInviteFiltering/Can_ignore_a_single_user"}
{"Action":"pass","Test":"TestInviteFiltering/Can_ignore_a_whole_server"}
{"Action":"pass","Test":"TestInviteFiltering/Can_invite_users_normally_without_any_rules"}
{"Action":"pass","Test":"TestInviteFiltering/Will_allow_users_when_a_user_appears_in_multiple_fields"}
{"Action":"pass","Test":"TestInviteFiltering/Will_ignore_null_fields"}
{"Action":"pass","Test":"TestInviteFromIgnoredUsersDoesNotAppearInSync"} {"Action":"pass","Test":"TestInviteFromIgnoredUsersDoesNotAppearInSync"}
{"Action":"pass","Test":"TestIsDirectFlagFederation"} {"Action":"pass","Test":"TestIsDirectFlagFederation"}
{"Action":"pass","Test":"TestIsDirectFlagLocal"} {"Action":"pass","Test":"TestIsDirectFlagLocal"}
@@ -214,18 +229,20 @@
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/federation/looking_backwards,_should_be_able_to_find_event_that_was_sent_before_we_joined"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/federation/looking_backwards,_should_be_able_to_find_event_that_was_sent_before_we_joined"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/federation/looking_forwards,_should_be_able_to_find_event_that_was_sent_before_we_joined"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/federation/looking_forwards,_should_be_able_to_find_event_that_was_sent_before_we_joined"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/federation/when_looking_backwards_before_the_room_was_created,_should_be_able_to_find_event_that_was_imported"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/federation/when_looking_backwards_before_the_room_was_created,_should_be_able_to_find_event_that_was_imported"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_after_given_timestmap"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_after_given_timestamp"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_before_given_timestmap"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_before_given_timestamp"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_next_event_topologically_after_given_timestmap_when_all_message_timestamps_are_the_same"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_next_event_topologically_after_given_timestamp_when_all_message_timestamps_are_the_same"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_next_event_topologically_before_given_timestamp_when_all_message_timestamps_are_the_same"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_next_event_topologically_before_given_timestamp_when_all_message_timestamps_are_the_same"}
{"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_after_the_latest_timestmap"} {"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_after_the_latest_timestamp"}
{"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_before_the_earliest_timestmap"} {"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_before_the_earliest_timestamp"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_not_be_able_to_query_a_private_room_you_are_not_a_member_of"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_not_be_able_to_query_a_private_room_you_are_not_a_member_of"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_not_be_able_to_query_a_public_room_you_are_not_a_member_of"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_not_be_able_to_query_a_public_room_you_are_not_a_member_of"}
{"Action":"fail","Test":"TestKeyChangesLocal"} {"Action":"fail","Test":"TestKeyChangesLocal"}
{"Action":"fail","Test":"TestKeyChangesLocal/New_login_should_create_a_device_lists.changed_entry"} {"Action":"fail","Test":"TestKeyChangesLocal/New_login_should_create_a_device_lists.changed_entry"}
{"Action":"fail","Test":"TestKeyClaimOrdering"} {"Action":"fail","Test":"TestKeyClaimOrdering"}
{"Action":"pass","Test":"TestKeysQueryWithDeviceIDAsObjectFails"} {"Action":"pass","Test":"TestKeysQueryWithDeviceIDAsObjectFails"}
{"Action":"pass","Test":"TestKnockRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV11"}
{"Action":"pass","Test":"TestKnockRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV12"}
{"Action":"fail","Test":"TestKnockRoomsInPublicRoomsDirectory"} {"Action":"fail","Test":"TestKnockRoomsInPublicRoomsDirectory"}
{"Action":"fail","Test":"TestKnockRoomsInPublicRoomsDirectoryInMSC3787Room"} {"Action":"fail","Test":"TestKnockRoomsInPublicRoomsDirectoryInMSC3787Room"}
{"Action":"fail","Test":"TestKnocking"} {"Action":"fail","Test":"TestKnocking"}
@@ -252,8 +269,8 @@
{"Action":"pass","Test":"TestKnocking/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail#01"} {"Action":"pass","Test":"TestKnocking/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail#01"}
{"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_join_rule_'knock'_should_succeed"} {"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_join_rule_'knock'_should_succeed"}
{"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_join_rule_'knock'_should_succeed#01"} {"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_join_rule_'knock'_should_succeed#01"}
{"Action":"pass","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock"} {"Action":"fail","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock"}
{"Action":"pass","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"} {"Action":"fail","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"}
{"Action":"fail","Test":"TestKnockingInMSC3787Room"} {"Action":"fail","Test":"TestKnockingInMSC3787Room"}
{"Action":"fail","Test":"TestKnockingInMSC3787Room/A_user_can_knock_on_a_room_without_a_reason"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/A_user_can_knock_on_a_room_without_a_reason"}
{"Action":"fail","Test":"TestKnockingInMSC3787Room/A_user_can_knock_on_a_room_without_a_reason#01"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/A_user_can_knock_on_a_room_without_a_reason#01"}
@@ -278,8 +295,8 @@
{"Action":"pass","Test":"TestKnockingInMSC3787Room/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail#01"} {"Action":"pass","Test":"TestKnockingInMSC3787Room/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail#01"}
{"Action":"fail","Test":"TestKnockingInMSC3787Room/Knocking_on_a_room_with_join_rule_'knock'_should_succeed"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/Knocking_on_a_room_with_join_rule_'knock'_should_succeed"}
{"Action":"fail","Test":"TestKnockingInMSC3787Room/Knocking_on_a_room_with_join_rule_'knock'_should_succeed#01"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/Knocking_on_a_room_with_join_rule_'knock'_should_succeed#01"}
{"Action":"pass","Test":"TestKnockingInMSC3787Room/Users_in_the_room_see_a_user's_membership_update_when_they_knock"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/Users_in_the_room_see_a_user's_membership_update_when_they_knock"}
{"Action":"pass","Test":"TestKnockingInMSC3787Room/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"}
{"Action":"pass","Test":"TestLeakyTyping"} {"Action":"pass","Test":"TestLeakyTyping"}
{"Action":"pass","Test":"TestLeaveEventInviteRejection"} {"Action":"pass","Test":"TestLeaveEventInviteRejection"}
{"Action":"fail","Test":"TestLeaveEventVisibility"} {"Action":"fail","Test":"TestLeaveEventVisibility"}
@@ -308,10 +325,43 @@
{"Action":"pass","Test":"TestLogout/Request_to_logout_without_an_access_token_is_rejected"} {"Action":"pass","Test":"TestLogout/Request_to_logout_without_an_access_token_is_rejected"}
{"Action":"fail","Test":"TestMSC3757OwnedState"} {"Action":"fail","Test":"TestMSC3757OwnedState"}
{"Action":"pass","Test":"TestMSC3967"} {"Action":"pass","Test":"TestMSC3967"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/PL_event_is_missing_creator_in_users_map"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/admin_with_>PL100_cannot_kick_creator"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators/admin_with_>PL100_sorts_after_the_room_creator_for_state_resolution"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/creator_can_kick_admin"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/creator_can_kick_admin_above_PL100"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/creator_can_kick_admin_at_JSON_max_value"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators/creator_cannot_set_self_in_PL_event"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/m.room.tombstone_needs_PL150_in_the_PL_event"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators/power_level_cannot_be_set_beyond_max_canonical_JSON_int"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators/power_level_content_override_can_be_set"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators/power_level_content_override_cannot_set_the_room_creator"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_Additional"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalCreatorsAndInvited"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation"}
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_are_valid"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_elements_aren't_strings"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_elements_aren't_user_ID_strings"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_elements_aren't_valid_user_ID_strings_(domain)"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_isn't_an_array"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_InvitedAreCreators"}
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_Upgrades"}
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent"}
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent"}
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent"}
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent"}
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms"}
{"Action":"fail","Test":"TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph"}
{"Action":"fail","Test":"TestMSC4297StateResolutionV2_1_starts_from_empty_set"}
{"Action":"fail","Test":"TestMSC4308ThreadSubscriptionsSlidingSync"}
{"Action":"fail","Test":"TestMSC4308ThreadSubscriptionsSlidingSync/Receives_thread_subscriptions_over_incremental_sliding_sync"}
{"Action":"fail","Test":"TestMSC4308ThreadSubscriptionsSlidingSync/Receives_thread_subscriptions_over_initial_sliding_sync"}
{"Action":"fail","Test":"TestMSC4311FullCreateEventOnStrippedState"}
{"Action":"pass","Test":"TestMediaConfig"} {"Action":"pass","Test":"TestMediaConfig"}
{"Action":"pass","Test":"TestMediaFilenames"} {"Action":"fail","Test":"TestMediaFilenames"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel"} {"Action":"fail","Test":"TestMediaFilenames/Parallel"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII"} {"Action":"fail","Test":"TestMediaFilenames/Parallel/ASCII"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'ascii'"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'ascii'"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'ascii'_over_/_matrix/client/v1/media/download"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'ascii'_over_/_matrix/client/v1/media/download"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'name;with;semicolons'"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'name;with;semicolons'"}
@@ -319,11 +369,11 @@
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'name_with_spaces'"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'name_with_spaces'"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'name_with_spaces'_over_/_matrix/client/v1/media/download"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_file_'name_with_spaces'_over_/_matrix/client/v1/media/download"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_specifying_a_different_ASCII_file_name"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_specifying_a_different_ASCII_file_name"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_specifying_a_different_ASCII_file_name_over__matrix/client/v1/media/download"} {"Action":"fail","Test":"TestMediaFilenames/Parallel/ASCII/Can_download_specifying_a_different_ASCII_file_name_over__matrix/client/v1/media/download"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_upload_with_ASCII_file_name"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/ASCII/Can_upload_with_ASCII_file_name"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode"} {"Action":"fail","Test":"TestMediaFilenames/Parallel/Unicode"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_specifying_a_different_Unicode_file_name"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_specifying_a_different_Unicode_file_name"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_specifying_a_different_Unicode_file_name_over__matrix/client/v1/media/download"} {"Action":"fail","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_specifying_a_different_Unicode_file_name_over__matrix/client/v1/media/download"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_with_Unicode_file_name_locally"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_with_Unicode_file_name_locally"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_with_Unicode_file_name_locally_over__matrix/client/v1/media/download"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_with_Unicode_file_name_locally_over__matrix/client/v1/media/download"}
{"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_with_Unicode_file_name_over_federation"} {"Action":"pass","Test":"TestMediaFilenames/Parallel/Unicode/Can_download_with_Unicode_file_name_over_federation"}
@@ -352,9 +402,15 @@
{"Action":"pass","Test":"TestMembersLocal/Parallel/Existing_members_see_new_members'_presence_(in_initial_sync)"} {"Action":"pass","Test":"TestMembersLocal/Parallel/Existing_members_see_new_members'_presence_(in_initial_sync)"}
{"Action":"pass","Test":"TestMembersLocal/Parallel/New_room_members_see_their_own_join_event"} {"Action":"pass","Test":"TestMembersLocal/Parallel/New_room_members_see_their_own_join_event"}
{"Action":"fail","Test":"TestMembershipOnEvents"} {"Action":"fail","Test":"TestMembershipOnEvents"}
{"Action":"fail","Test":"TestNetworkPartitionOrdering"} {"Action":"fail","Test":"TestMessagesOverFederation"}
{"Action":"pass","Test":"TestMessagesOverFederation/Visible_shared_history_after_joining_new_room_(backfill)"}
{"Action":"pass","Test":"TestMessagesOverFederation/Visible_shared_history_after_joining_new_room_(backfill)/`messagesRequestLimit`_is_greater_than_the_number_of_messages_backfilled_(in_Synapse,_100)"}
{"Action":"pass","Test":"TestMessagesOverFederation/Visible_shared_history_after_joining_new_room_(backfill)/`messagesRequestLimit`_is_lower_than_the_number_of_messages_backfilled_(assumed)"}
{"Action":"fail","Test":"TestMessagesOverFederation/Visible_shared_history_after_re-joining_room_(backfill)"}
{"Action":"fail","Test":"TestMessagesOverFederation/Visible_shared_history_after_re-joining_room_(backfill)/`messagesRequestLimit`_is_lower_than_the_number_of_messages_backfilled_(assumed)"}
{"Action":"pass","Test":"TestNetworkPartitionOrdering"}
{"Action":"pass","Test":"TestNotPresentUserCannotBanOthers"} {"Action":"pass","Test":"TestNotPresentUserCannotBanOthers"}
{"Action":"pass","Test":"TestOlderLeftRoomsNotInLeaveSection"} {"Action":"fail","Test":"TestOlderLeftRoomsNotInLeaveSection"}
{"Action":"fail","Test":"TestOutboundFederationEventSizeGetMissingEvents"} {"Action":"fail","Test":"TestOutboundFederationEventSizeGetMissingEvents"}
{"Action":"fail","Test":"TestOutboundFederationIgnoresMissingEventWithBadJSONForRoomVersion6"} {"Action":"fail","Test":"TestOutboundFederationIgnoresMissingEventWithBadJSONForRoomVersion6"}
{"Action":"pass","Test":"TestOutboundFederationProfile"} {"Action":"pass","Test":"TestOutboundFederationProfile"}
@@ -379,7 +435,23 @@
{"Action":"pass","Test":"TestProfileDisplayName"} {"Action":"pass","Test":"TestProfileDisplayName"}
{"Action":"pass","Test":"TestProfileDisplayName/GET_/profile/:user_id/displayname_publicly_accessible"} {"Action":"pass","Test":"TestProfileDisplayName/GET_/profile/:user_id/displayname_publicly_accessible"}
{"Action":"pass","Test":"TestProfileDisplayName/PUT_/profile/:user_id/displayname_sets_my_name"} {"Action":"pass","Test":"TestProfileDisplayName/PUT_/profile/:user_id/displayname_sets_my_name"}
{"Action":"pass","Test":"TestPublicRooms"}
{"Action":"pass","Test":"TestPublicRooms/Can_search_public_room_list"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroom_with_unicode_chars_name"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroom_with_unicode_chars_name_topic"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroom_with_unicode_chars_topic"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroomalias_no_name"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroomalias_with_name"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroomalias_with_name_topic"}
{"Action":"pass","Test":"TestPublicRooms/Name/topic_keys_are_correct/Creating_room_with_alias_publicroomalias_with_topic"}
{"Action":"pass","Test":"TestPushRuleCacheHealth"} {"Action":"pass","Test":"TestPushRuleCacheHealth"}
{"Action":"fail","Test":"TestPushRuleRoomUpgrade"}
{"Action":"fail","Test":"TestPushRuleRoomUpgrade/parallel"}
{"Action":"fail","Test":"TestPushRuleRoomUpgrade/parallel/joining_a_remote_manually_upgraded_room_carries_over_existing_push_rules"}
{"Action":"fail","Test":"TestPushRuleRoomUpgrade/parallel/joining_a_remote_upgraded_room_carries_over_existing_push_rules"}
{"Action":"fail","Test":"TestPushRuleRoomUpgrade/parallel/manually_upgrading_a_room_carries_over_existing_push_rules_for_local_users"}
{"Action":"fail","Test":"TestPushRuleRoomUpgrade/parallel/upgrading_a_room_carries_over_existing_push_rules_for_local_users"}
{"Action":"pass","Test":"TestPushSync"} {"Action":"pass","Test":"TestPushSync"}
{"Action":"pass","Test":"TestPushSync/Adding_a_push_rule_wakes_up_an_incremental_/sync"} {"Action":"pass","Test":"TestPushSync/Adding_a_push_rule_wakes_up_an_incremental_/sync"}
{"Action":"pass","Test":"TestPushSync/Disabling_a_push_rule_wakes_up_an_incremental_/sync"} {"Action":"pass","Test":"TestPushSync/Disabling_a_push_rule_wakes_up_an_incremental_/sync"}
@@ -440,14 +512,16 @@
{"Action":"pass","Test":"TestRestrictedRoomsLocalJoinInMSC3787Room/Join_should_fail_with_mangled_join_rules"} {"Action":"pass","Test":"TestRestrictedRoomsLocalJoinInMSC3787Room/Join_should_fail_with_mangled_join_rules"}
{"Action":"pass","Test":"TestRestrictedRoomsLocalJoinInMSC3787Room/Join_should_succeed_when_invited"} {"Action":"pass","Test":"TestRestrictedRoomsLocalJoinInMSC3787Room/Join_should_succeed_when_invited"}
{"Action":"fail","Test":"TestRestrictedRoomsLocalJoinInMSC3787Room/Join_should_succeed_when_joined_to_allowed_room"} {"Action":"fail","Test":"TestRestrictedRoomsLocalJoinInMSC3787Room/Join_should_succeed_when_joined_to_allowed_room"}
{"Action":"pass","Test":"TestRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV11"}
{"Action":"pass","Test":"TestRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV12"}
{"Action":"fail","Test":"TestRestrictedRoomsRemoteJoin"} {"Action":"fail","Test":"TestRestrictedRoomsRemoteJoin"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_fail_initially"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_fail_initially"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_fail_when_left_allowed_room"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_fail_when_left_allowed_room"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_fail_with_mangled_join_rules"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_fail_with_mangled_join_rules"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_succeed_when_invited"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoin/Join_should_succeed_when_invited"}
{"Action":"fail","Test":"TestRestrictedRoomsRemoteJoin/Join_should_succeed_when_joined_to_allowed_room"} {"Action":"fail","Test":"TestRestrictedRoomsRemoteJoin/Join_should_succeed_when_joined_to_allowed_room"}
{"Action":"fail","Test":"TestRestrictedRoomsRemoteJoinFailOver"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinFailOver"}
{"Action":"fail","Test":"TestRestrictedRoomsRemoteJoinFailOverInMSC3787Room"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinFailOverInMSC3787Room"}
{"Action":"fail","Test":"TestRestrictedRoomsRemoteJoinInMSC3787Room"} {"Action":"fail","Test":"TestRestrictedRoomsRemoteJoinInMSC3787Room"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinInMSC3787Room/Join_should_fail_initially"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinInMSC3787Room/Join_should_fail_initially"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinInMSC3787Room/Join_should_fail_when_left_allowed_room"} {"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinInMSC3787Room/Join_should_fail_when_left_allowed_room"}
@@ -475,16 +549,19 @@
{"Action":"fail","Test":"TestRoomCanonicalAlias/Parallel/m.room.canonical_alias_rejects_missing_aliases"} {"Action":"fail","Test":"TestRoomCanonicalAlias/Parallel/m.room.canonical_alias_rejects_missing_aliases"}
{"Action":"fail","Test":"TestRoomCanonicalAlias/Parallel/m.room.canonical_alias_rejects_missing_aliases#01"} {"Action":"fail","Test":"TestRoomCanonicalAlias/Parallel/m.room.canonical_alias_rejects_missing_aliases#01"}
{"Action":"fail","Test":"TestRoomCanonicalAlias/Parallel/m.room.canonical_alias_setting_rejects_deleted_aliases"} {"Action":"fail","Test":"TestRoomCanonicalAlias/Parallel/m.room.canonical_alias_setting_rejects_deleted_aliases"}
{"Action":"pass","Test":"TestRoomCreate"} {"Action":"fail","Test":"TestRoomCreate"}
{"Action":"pass","Test":"TestRoomCreate/Parallel"} {"Action":"fail","Test":"TestRoomCreate/Parallel"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/Can_/sync_newly_created_room"} {"Action":"pass","Test":"TestRoomCreate/Parallel/Can_/sync_newly_created_room"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_creates_a_room_with_the_given_version"} {"Action":"fail","Test":"TestRoomCreate/Parallel/POST_/createRoom_creates_a_room_with_the_given_version"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_ignores_attempts_to_set_the_room_version_via_creation_content"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_ignores_attempts_to_set_the_room_version_via_creation_content"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_private_room"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_private_room"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_private_room_with_invites"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_private_room_with_invites"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_public_room"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_public_room"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_name"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_name"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic"}
{"Action":"fail","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic_and_writes_rich_topic_representation"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic_via_initial_state"}
{"Action":"fail","Test":"TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic_via_initial_state_overwritten_by_topic"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_rejects_attempts_to_create_rooms_with_numeric_versions"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_rejects_attempts_to_create_rooms_with_numeric_versions"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_rejects_attempts_to_create_rooms_with_unknown_versions"} {"Action":"pass","Test":"TestRoomCreate/Parallel/POST_/createRoom_rejects_attempts_to_create_rooms_with_unknown_versions"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/Rooms_can_be_created_with_an_initial_invite_list_(SYN-205)"} {"Action":"pass","Test":"TestRoomCreate/Parallel/Rooms_can_be_created_with_an_initial_invite_list_(SYN-205)"}
@@ -527,6 +604,7 @@
{"Action":"pass","Test":"TestRoomMessagesLazyLoadingLocalUser"} {"Action":"pass","Test":"TestRoomMessagesLazyLoadingLocalUser"}
{"Action":"pass","Test":"TestRoomReadMarkers"} {"Action":"pass","Test":"TestRoomReadMarkers"}
{"Action":"pass","Test":"TestRoomReceipts"} {"Action":"pass","Test":"TestRoomReceipts"}
{"Action":"pass","Test":"TestRoomReceipts/Receipts_DO_NOT_include_a_`room_id`_field"}
{"Action":"pass","Test":"TestRoomSpecificUsernameAtJoin"} {"Action":"pass","Test":"TestRoomSpecificUsernameAtJoin"}
{"Action":"pass","Test":"TestRoomSpecificUsernameAtJoin/Bob_can_find_Alice_by_mxid"} {"Action":"pass","Test":"TestRoomSpecificUsernameAtJoin/Bob_can_find_Alice_by_mxid"}
{"Action":"pass","Test":"TestRoomSpecificUsernameAtJoin/Bob_can_find_Alice_by_profile_display_name"} {"Action":"pass","Test":"TestRoomSpecificUsernameAtJoin/Bob_can_find_Alice_by_profile_display_name"}
@@ -575,7 +653,7 @@
{"Action":"pass","Test":"TestSearch/parallel/Search_results_with_recent_ordering_do_not_include_redacted_events"} {"Action":"pass","Test":"TestSearch/parallel/Search_results_with_recent_ordering_do_not_include_redacted_events"}
{"Action":"pass","Test":"TestSearch/parallel/Search_works_across_an_upgraded_room_and_its_predecessor"} {"Action":"pass","Test":"TestSearch/parallel/Search_works_across_an_upgraded_room_and_its_predecessor"}
{"Action":"fail","Test":"TestSendAndFetchMessage"} {"Action":"fail","Test":"TestSendAndFetchMessage"}
{"Action":"skip","Test":"TestSendJoinPartialStateResponse"} {"Action":"fail","Test":"TestSendJoinPartialStateResponse"}
{"Action":"pass","Test":"TestSendMessageWithTxn"} {"Action":"pass","Test":"TestSendMessageWithTxn"}
{"Action":"pass","Test":"TestServerCapabilities"} {"Action":"pass","Test":"TestServerCapabilities"}
{"Action":"skip","Test":"TestServerNotices"} {"Action":"skip","Test":"TestServerNotices"}
@@ -589,7 +667,7 @@
{"Action":"fail","Test":"TestSync/parallel/Newly_joined_room_has_correct_timeline_in_incremental_sync"} {"Action":"fail","Test":"TestSync/parallel/Newly_joined_room_has_correct_timeline_in_incremental_sync"}
{"Action":"fail","Test":"TestSync/parallel/Newly_joined_room_includes_presence_in_incremental_sync"} {"Action":"fail","Test":"TestSync/parallel/Newly_joined_room_includes_presence_in_incremental_sync"}
{"Action":"pass","Test":"TestSync/parallel/Newly_joined_room_is_included_in_an_incremental_sync"} {"Action":"pass","Test":"TestSync/parallel/Newly_joined_room_is_included_in_an_incremental_sync"}
{"Action":"pass","Test":"TestSync/parallel/sync_should_succeed_even_if_the_sync_token_points_to_a_redaction_of_an_unknown_event"} {"Action":"fail","Test":"TestSync/parallel/sync_should_succeed_even_if_the_sync_token_points_to_a_redaction_of_an_unknown_event"}
{"Action":"pass","Test":"TestSyncFilter"} {"Action":"pass","Test":"TestSyncFilter"}
{"Action":"pass","Test":"TestSyncFilter/Can_create_filter"} {"Action":"pass","Test":"TestSyncFilter/Can_create_filter"}
{"Action":"pass","Test":"TestSyncFilter/Can_download_filter"} {"Action":"pass","Test":"TestSyncFilter/Can_download_filter"}
@@ -603,12 +681,21 @@
{"Action":"pass","Test":"TestSyncTimelineGap/incremental"} {"Action":"pass","Test":"TestSyncTimelineGap/incremental"}
{"Action":"pass","Test":"TestTentativeEventualJoiningAfterRejecting"} {"Action":"pass","Test":"TestTentativeEventualJoiningAfterRejecting"}
{"Action":"fail","Test":"TestThreadReceiptsInSyncMSC4102"} {"Action":"fail","Test":"TestThreadReceiptsInSyncMSC4102"}
{"Action":"fail","Test":"TestThreadSubscriptions"}
{"Action":"fail","Test":"TestThreadSubscriptions/Can_create_automatic_subscription_to_a_thread"}
{"Action":"fail","Test":"TestThreadSubscriptions/Can_subscribe_to_and_unsubscribe_from_a_thread"}
{"Action":"fail","Test":"TestThreadSubscriptions/Cannot_use_thread_root_as_automatic_subscription_cause_event"}
{"Action":"fail","Test":"TestThreadSubscriptions/Error_when_using_invalid_automatic_event_ID"}
{"Action":"fail","Test":"TestThreadSubscriptions/Manual_subscriptions_overwrite_automatic_subscriptions"}
{"Action":"pass","Test":"TestThreadSubscriptions/Nonexistent_threads_return_404"}
{"Action":"fail","Test":"TestThreadSubscriptions/Server-side_automatic_subscription_ordering_conflict"}
{"Action":"fail","Test":"TestThreadSubscriptions/Unsubscribe_succeeds_even_with_no_subscription"}
{"Action":"fail","Test":"TestThreadedReceipts"} {"Action":"fail","Test":"TestThreadedReceipts"}
{"Action":"fail","Test":"TestThreadsEndpoint"} {"Action":"fail","Test":"TestThreadsEndpoint"}
{"Action":"pass","Test":"TestToDeviceMessages"} {"Action":"pass","Test":"TestToDeviceMessages"}
{"Action":"fail","Test":"TestToDeviceMessagesOverFederation"} {"Action":"fail","Test":"TestToDeviceMessagesOverFederation"}
{"Action":"pass","Test":"TestToDeviceMessagesOverFederation/good_connectivity"} {"Action":"pass","Test":"TestToDeviceMessagesOverFederation/good_connectivity"}
{"Action":"pass","Test":"TestToDeviceMessagesOverFederation/interrupted_connectivity"} {"Action":"fail","Test":"TestToDeviceMessagesOverFederation/interrupted_connectivity"}
{"Action":"fail","Test":"TestToDeviceMessagesOverFederation/stopped_server"} {"Action":"fail","Test":"TestToDeviceMessagesOverFederation/stopped_server"}
{"Action":"fail","Test":"TestTxnIdWithRefreshToken"} {"Action":"fail","Test":"TestTxnIdWithRefreshToken"}
{"Action":"fail","Test":"TestTxnIdempotency"} {"Action":"fail","Test":"TestTxnIdempotency"}
@@ -617,6 +704,7 @@
{"Action":"pass","Test":"TestTxnScopeOnLocalEcho"} {"Action":"pass","Test":"TestTxnScopeOnLocalEcho"}
{"Action":"pass","Test":"TestTyping"} {"Action":"pass","Test":"TestTyping"}
{"Action":"pass","Test":"TestTyping/Typing_can_be_explicitly_stopped"} {"Action":"pass","Test":"TestTyping/Typing_can_be_explicitly_stopped"}
{"Action":"pass","Test":"TestTyping/Typing_events_DO_NOT_include_a_`room_id`_field"}
{"Action":"pass","Test":"TestTyping/Typing_notification_sent_to_local_room_members"} {"Action":"pass","Test":"TestTyping/Typing_notification_sent_to_local_room_members"}
{"Action":"fail","Test":"TestUnknownEndpoints"} {"Action":"fail","Test":"TestUnknownEndpoints"}
{"Action":"pass","Test":"TestUnknownEndpoints/Client-server_endpoints"} {"Action":"pass","Test":"TestUnknownEndpoints/Client-server_endpoints"}
@@ -624,7 +712,7 @@
{"Action":"pass","Test":"TestUnknownEndpoints/Media_endpoints"} {"Action":"pass","Test":"TestUnknownEndpoints/Media_endpoints"}
{"Action":"pass","Test":"TestUnknownEndpoints/Server-server_endpoints"} {"Action":"pass","Test":"TestUnknownEndpoints/Server-server_endpoints"}
{"Action":"pass","Test":"TestUnknownEndpoints/Unknown_prefix"} {"Action":"pass","Test":"TestUnknownEndpoints/Unknown_prefix"}
{"Action":"fail","Test":"TestUnrejectRejectedEvents"} {"Action":"pass","Test":"TestUnrejectRejectedEvents"}
{"Action":"fail","Test":"TestUploadKey"} {"Action":"fail","Test":"TestUploadKey"}
{"Action":"fail","Test":"TestUploadKey/Parallel"} {"Action":"fail","Test":"TestUploadKey/Parallel"}
{"Action":"fail","Test":"TestUploadKey/Parallel/Can_claim_one_time_key_using_POST"} {"Action":"fail","Test":"TestUploadKey/Parallel/Can_claim_one_time_key_using_POST"}
@@ -637,7 +725,7 @@
{"Action":"pass","Test":"TestUploadKeyIdempotency"} {"Action":"pass","Test":"TestUploadKeyIdempotency"}
{"Action":"pass","Test":"TestUploadKeyIdempotencyOverlap"} {"Action":"pass","Test":"TestUploadKeyIdempotencyOverlap"}
{"Action":"fail","Test":"TestUrlPreview"} {"Action":"fail","Test":"TestUrlPreview"}
{"Action":"pass","Test":"TestUserAppearsInChangedDeviceListOnJoinOverFederation"} {"Action":"fail","Test":"TestUserAppearsInChangedDeviceListOnJoinOverFederation"}
{"Action":"pass","Test":"TestVersionStructure"} {"Action":"pass","Test":"TestVersionStructure"}
{"Action":"pass","Test":"TestVersionStructure/Version_responds_200_OK_with_valid_structure"} {"Action":"pass","Test":"TestVersionStructure/Version_responds_200_OK_with_valid_structure"}
{"Action":"pass","Test":"TestWithoutOwnedState"} {"Action":"pass","Test":"TestWithoutOwnedState"}