Compare commits

..

1 Commits

Author SHA1 Message Date
Jade Ellis 7a8c409ff9 refactor: Use snafu
This should gradtly improve the debugging experience by allowing
tracking backtraces to get more context from the error.
A lot of the touced files here are just cleaning up the old way of
creating errors.
2026-02-22 14:06:15 +00:00
90 changed files with 1219 additions and 1435 deletions
Generated
+42 -19
View File
@@ -445,14 +445,13 @@ dependencies = [
[[package]] [[package]]
name = "axum-extra" name = "axum-extra"
version = "0.12.5" version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
dependencies = [ dependencies = [
"axum", "axum",
"axum-core", "axum-core",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"headers", "headers",
"http", "http",
@@ -460,6 +459,8 @@ 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",
@@ -1013,6 +1014,7 @@ 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",
@@ -1026,7 +1028,7 @@ dependencies = [
"serde_regex", "serde_regex",
"smallstr", "smallstr",
"smallvec", "smallvec",
"thiserror 2.0.18", "snafu",
"tikv-jemalloc-ctl", "tikv-jemalloc-ctl",
"tikv-jemalloc-sys", "tikv-jemalloc-sys",
"tikv-jemallocator", "tikv-jemallocator",
@@ -1153,7 +1155,7 @@ dependencies = [
"conduwuit_service", "conduwuit_service",
"futures", "futures",
"rand 0.10.0", "rand 0.10.0",
"thiserror 2.0.18", "snafu",
"tracing", "tracing",
] ]
@@ -1221,7 +1223,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"ruma-common", "ruma-common",
"serde", "serde",
@@ -1600,7 +1602,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"ruma-common", "ruma-common",
"serde", "serde",
@@ -3002,7 +3004,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"ruma-common", "ruma-common",
"serde", "serde",
@@ -4094,7 +4096,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"assign", "assign",
"continuwuity-admin-api", "continuwuity-admin-api",
@@ -4117,7 +4119,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4129,7 +4131,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@@ -4152,7 +4154,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@@ -4184,7 +4186,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap", "indexmap",
@@ -4209,7 +4211,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@@ -4231,7 +4233,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror 2.0.18", "thiserror 2.0.18",
@@ -4240,7 +4242,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4250,7 +4252,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-crate", "proc-macro-crate",
@@ -4265,7 +4267,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4277,7 +4279,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=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=e087ff15888156942ca2ffe6097d1b4c3fd27628#e087ff15888156942ca2ffe6097d1b4c3fd27628"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
@@ -4910,6 +4912,27 @@ 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"
+9 -5
View File
@@ -97,7 +97,7 @@ features = [
] ]
[workspace.dependencies.axum-extra] [workspace.dependencies.axum-extra]
version = "0.12.0" version = "0.10.1"
default-features = false default-features = false
features = ["typed-header", "tracing"] features = ["typed-header", "tracing"]
@@ -307,9 +307,14 @@ features = [
] ]
# Used for conduwuit::Error type # Used for conduwuit::Error type
[workspace.dependencies.thiserror] [workspace.dependencies.snafu]
version = "2.0.12" version = "0.8"
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]
@@ -343,7 +348,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 = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88" rev = "e087ff15888156942ca2ffe6097d1b4c3fd27628"
features = [ features = [
"compat", "compat",
"rand", "rand",
@@ -381,7 +386,6 @@ 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]
+3 -8
View File
@@ -57,15 +57,10 @@ Continuwuity aims to:
### Can I try it out? ### Can I try it out?
Check out the [documentation](https://continuwuity.org) for installation instructions. 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!
If you want to try it out as a user, we have some partnered homeservers you can use: - https://continuwuity.rocks -- A public demo server operated by the Continuwuity Team.
* You can head over to [https://federated.nexus](https://federated.nexus/) in your browser. - https://federated.nexus -- Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo.
* 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:-tests/test_results/complement/test_logs.jsonl}" LOG_FILE="${2:-complement_test_logs.jsonl}"
# A `.jsonl` file to write test results to # A `.jsonl` file to write test results to
RESULTS_FILE="${3:-tests/test_results/complement/test_results.jsonl}" RESULTS_FILE="${3:-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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim.
-1
View File
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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,9 +9,10 @@ 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,conduwuit_database=warn,conduwuit_service::manager=info,conduwuit_api::router=error,conduwuit_router=error,tower_http=error" log = "trace,h2=debug,hyper=debug"
port = [8008, 8448] port = [8008, 8448]
trusted_servers = [] trusted_servers = []
only_query_trusted_key_servers = false only_query_trusted_key_servers = false
@@ -24,7 +25,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 = false log_colors = true
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
@@ -47,7 +48,6 @@ 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
+12 -41
View File
@@ -290,25 +290,6 @@
# #
#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.
# #
@@ -546,6 +527,12 @@
# #
#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.
@@ -1844,13 +1831,14 @@
# #
#support_mxid = #support_mxid =
# **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
#
# A list of MatrixRTC foci URLs which will be served as part of the # A list of MatrixRTC foci URLs which will be served as part of the
# MSC4143 client endpoint at /.well-known/matrix/client. # 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" },
# ]
# #
# This option is deprecated and will be removed in a future release. # To disable, set this to be an empty vector (`[]`).
# Please migrate to the new `[global.matrix_rtc]` config section.
# #
#rtc_focus_server_urls = [] #rtc_focus_server_urls = []
@@ -1872,23 +1860,6 @@
# #
#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.
+32 -4
View File
@@ -78,19 +78,47 @@ 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 the `[global.matrix_rtc]` config section using the `foci` option. 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`.
The variable should be a list of servers serving as MatrixRTC endpoints. Clients discover these via the `/_matrix/client/v1/rtc/transports` endpoint (MSC4143). The variable should be a list of servers serving as MatrixRTC endpoints to serve in the well-known file to the client.
```toml ```toml
[global.matrix_rtc] rtc_focus_server_urls = [
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.
+1 -7
View File
@@ -51,13 +51,7 @@ continuwuity aims to:
Check out the [documentation](https://continuwuity.org) for installation instructions. Check out the [documentation](https://continuwuity.org) for installation instructions.
If you want to try it out as a user, we have some partnered homeservers you can use: There are currently no open registration continuwuity instances available.
* 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,7 +36,3 @@ 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
+2 -5
View File
@@ -30,15 +30,12 @@ pub(super) async fn incoming_federation(&self) -> Result {
.federation_handletime .federation_handletime
.read(); .read();
let mut msg = format!( let mut msg = format!("Handling {} incoming pdus:\n", map.len());
"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
}; };
+1 -19
View File
@@ -29,9 +29,7 @@ pub(super) async fn delete(
.delete(&mxc.as_str().try_into()?) .delete(&mxc.as_str().try_into()?)
.await?; .await?;
return self return Err!("Deleted the MXC from our database and on our filesystem.",);
.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 {
@@ -390,19 +388,3 @@ 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,16 +108,4 @@ 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::<conduwuit::Error>() .try_stream()
.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())
+1 -11
View File
@@ -20,17 +20,7 @@ pub enum ResolverCommand {
name: Option<String>, name: Option<String>,
}, },
/// Flush a given server from the resolver caches or flush them completely /// Flush a specific server from the resolver caches or everything
///
/// * 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, Error, Event, Result, debug_info, err, error, info, Err, 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(Error::Uiaa(uiaainfo)); return Err!(Uiaa(uiaainfo));
} }
// Success! // Success!
}, },
@@ -401,7 +401,7 @@ pub(crate) async fn register_route(
&uiaainfo, &uiaainfo,
json, json,
); );
return Err(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(Request(NotJson("JSON body is not valid"))); return Err!(Request(NotJson("JSON body is not valid")));
+7 -1
View File
@@ -9,7 +9,7 @@ use ruma::{
}, },
events::{ events::{
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent, AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
RoomAccountDataEventType, GlobalAccountDataEventType, RoomAccountDataEventType,
}, },
serde::Raw, serde::Raw,
}; };
@@ -126,6 +126,12 @@ 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, Error, Result, debug, err, utils}; use conduwuit::{Err, 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(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); return Err!(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, Error, Result, debug, debug_warn, err, Err, 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(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); return Err!(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(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?; .map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "Invalid `from`.")))?;
let to = body let to = body
.to .to
.parse() .parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?; .map_err(|_| err!(BadRequest(ErrorKind::InvalidParam, "Invalid `to`.")))?;
device_list_updates.extend( device_list_updates.extend(
services services
+5 -27
View File
@@ -3,10 +3,9 @@ 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, Err, Result, err, error,
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},
@@ -70,7 +69,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
{ {
err!("Failed to save uploaded media: {e}"); error!("Failed to save uploaded media: {e}");
return Err!(Request(Unknown("Failed to save uploaded media"))); return Err!(Request(Unknown("Failed to save uploaded media")));
} }
@@ -145,22 +144,12 @@ 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,
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await { } = 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"),
@@ -196,18 +185,7 @@ pub(crate) async fn get_content_as_filename_route(
content, content,
content_type, content_type,
content_disposition, content_disposition,
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await { } = fetch_file(&services, &mxc, user, body.timeout_ms, Some(&body.filename)).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, Error, Result, at, debug_warn, Err, 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(Error::BadRequest( return Err!(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(Error::BadRequest( return Err!(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, Error, Result, err}; use conduwuit::{Err, 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 => Error::BadRequest( | InsertPushRuleError::ServerDefaultRuleId => err!(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 => Error::BadRequest( | InsertPushRuleError::InvalidRuleId => err!(BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Rule ID containing invalid characters.", "Rule ID containing invalid characters.",
), )),
| InsertPushRuleError::RelativeToServerDefaultRule => Error::BadRequest( | InsertPushRuleError::RelativeToServerDefaultRule => err!(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 => Error::BadRequest( | InsertPushRuleError::UnknownRuleId => err!(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 => Error::BadRequest( | InsertPushRuleError::BeforeHigherThanAfter => err!(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.",
), )),
| _ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."), | _ => err!(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 => Error::BadRequest( | RemovePushRuleError::ServerDefault => err!(BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Cannot delete a server-default pushrule.", "Cannot delete a server-default pushrule.",
), )),
| RemovePushRuleError::NotFound => | RemovePushRuleError::NotFound =>
Error::BadRequest(ErrorKind::NotFound, "Push rule not found."), err!(BadRequest(ErrorKind::NotFound, "Push rule not found.")),
| _ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."), | _ => err!(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, Error, Event, Result, RoomVersion, debug, err, info, Err, 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(Error::BadRequest( return Err!(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}");
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event") err!(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(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?, .map_err(|_| err!(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(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?, .map_err(|_| err!(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(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")); return Err!(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
.transactions .transaction_ids
.get_client_txn(sender_user, sender_device, &body.txn_id) .existing_txnid(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.transactions.add_client_txnid( services.transaction_ids.add_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, Error, Result, debug, err, info, Err, 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(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked.")); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(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(Error::Uiaa(uiaainfo)); return Err!(Uiaa(uiaainfo));
}, },
| _ => { | _ => {
return Err!(Request(NotJson("No JSON body was sent when required."))); return Err!(Request(NotJson("No JSON body was sent when required.")));
+1 -3
View File
@@ -336,9 +336,7 @@ where
let ranges = list.ranges.clone(); let ranges = list.ranges.clone();
for mut range in ranges { for mut range in ranges {
range.0 = range range.0 = uint!(0);
.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::{Error, Result}; use conduwuit::{Result, err};
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
.transactions .transaction_ids
.get_client_txn(sender_user, sender_device, &body.txn_id) .existing_txnid(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(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid"))?; .map_err(|_| err!(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
.transactions .transaction_ids
.add_client_txnid(sender_user, sender_device, &body.txn_id, &[]); .add_txnid(sender_user, sender_device, &body.txn_id, &[]);
Ok(send_event_to_device::v3::Response {}) Ok(send_event_to_device::v3::Response {})
} }
+8 -33
View File
@@ -1,11 +1,8 @@
use axum::{Json, extract::State, response::IntoResponse}; use axum::{Json, extract::State, response::IntoResponse};
use conduwuit::{Error, Result}; use conduwuit::{Err, Result};
use ruma::api::client::{ use ruma::api::client::discovery::{
discovery::{ discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo}, discover_support::{self, Contact},
discover_support::{self, Contact},
},
error::ErrorKind,
}; };
use crate::Ruma; use crate::Ruma;
@@ -19,7 +16,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(Error::BadRequest(ErrorKind::NotFound, "Not found.")), | None => return Err!(BadRequest(ErrorKind::NotFound, "Not found.")),
}; };
Ok(discover_homeserver::Response { Ok(discover_homeserver::Response {
@@ -27,32 +24,10 @@ 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 rtc_foci: services.config.well_known.rtc_focus_server_urls.clone(),
.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.
@@ -110,7 +85,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(Error::BadRequest(ErrorKind::NotFound, "Not found.")); return Err!(BadRequest(ErrorKind::NotFound, "Not found."));
} }
Ok(discover_support::Response { contacts, support_page }) Ok(discover_support::Response { contacts, support_page })
@@ -127,7 +102,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(Error::BadRequest(ErrorKind::NotFound, "Not found.")), | None => return Err!(BadRequest(ErrorKind::NotFound, "Not found.")),
}, },
}; };
-1
View File
@@ -184,7 +184,6 @@ 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)
+28 -46
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, Error, Result, debug_error, err, warn}; use conduwuit::{Err, Result, debug_error, err, warn};
use futures::{ use futures::{
TryFutureExt, TryFutureExt,
future::{ future::{
@@ -14,8 +14,7 @@ use futures::{
pin_mut, pin_mut,
}; };
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName, CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
OwnedUserId, UserId,
api::{ api::{
AuthScheme, IncomingRequest, Metadata, AuthScheme, IncomingRequest, Metadata,
client::{ client::{
@@ -67,17 +66,23 @@ 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 => {
match token { if !services
| Token::Appservice(_) | Token::User(_) => { .server
// we should have validated the token above .config
// already .allow_public_room_directory_without_auth
}, {
| Token::None | Token::Invalid => { match token {
return Err(Error::BadRequest( | Token::Appservice(_) | Token::User(_) => {
ErrorKind::MissingToken, // we should have validated the token above
"Missing or invalid access token.", // already
)); },
}, | Token::None | Token::Invalid => {
return Err!(BadRequest(
ErrorKind::MissingToken,
"Missing or invalid access token.",
));
},
}
} }
}, },
| &get_profile::v3::Request::METADATA | &get_profile::v3::Request::METADATA
@@ -91,7 +96,7 @@ pub(super) async fn auth(
// already // already
}, },
| Token::None | Token::Invalid => { | Token::None | Token::Invalid => {
return Err(Error::BadRequest( return Err!(BadRequest(
ErrorKind::MissingToken, ErrorKind::MissingToken,
"Missing or invalid access token.", "Missing or invalid access token.",
)); ));
@@ -125,10 +130,10 @@ pub(super) async fn auth(
appservice_info: None, appservice_info: None,
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")) Err!(BadRequest(ErrorKind::MissingToken, "Missing access token."))
} }
}, },
| _ => Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")), | _ => Err!(BadRequest(ErrorKind::MissingToken, "Missing access token.")),
}, },
| ( | (
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None, AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
@@ -144,7 +149,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(Error::BadRequest( return Err!(BadRequest(
ErrorKind::UserLocked, ErrorKind::UserLocked,
"This account has been locked.", "This account has been locked.",
)); ));
@@ -169,11 +174,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(Error::BadRequest( Err!(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(Error::BadRequest( | (AuthScheme::AppserviceToken, Token::User(_)) => Err!(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.",
)), )),
@@ -191,13 +196,13 @@ pub(super) async fn auth(
appservice_info: None, appservice_info: None,
}) })
} else { } else {
Err(Error::BadRequest( Err!(BadRequest(
ErrorKind::UnknownToken { soft_logout: false }, ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.", "Unknown access token.",
)) ))
} }
}, },
| (_, Token::Invalid) => Err(Error::BadRequest( | (_, Token::Invalid) => Err!(BadRequest(
ErrorKind::UnknownToken { soft_logout: false }, ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.", "Unknown access token.",
)), )),
@@ -229,33 +234,10 @@ 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, sender_device: None,
appservice_info: Some(*info), appservice_info: Some(*info),
}) })
} }
-4
View File
@@ -11,10 +11,6 @@ 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 {
+3 -6
View File
@@ -1,12 +1,9 @@
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, info, utils::stream::ReadyExt}; use conduwuit::{Err, Error, Result, err, info, utils::stream::ReadyExt};
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{RoomId, api::federation::authorization::get_event_authorization};
RoomId,
api::{client::error::ErrorKind, federation::authorization::get_event_authorization},
};
use super::AccessCheck; use super::AccessCheck;
use crate::Ruma; use crate::Ruma;
@@ -47,7 +44,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(|_| Error::BadRequest(ErrorKind::NotFound, "Event not found."))?; .map_err(|_| err!(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, Error, PduEvent, Result, err, error, Err, 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(Error::BadRequest( return Err!(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, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn}; use conduwuit::{Err, 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(Error::BadRequest( return Err!(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, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, warn}; use conduwuit::{Err, 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(Error::BadRequest( return Err!(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(Error::BadRequest( return Err!(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.",
)); ));
+11 -5
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::{Error, Result}; use conduwuit::{Err, Result, err};
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(Error::BadRequest(ErrorKind::forbidden(), "Room directory is not public")); return Err!(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,7 +38,10 @@ pub(crate) async fn get_public_rooms_filtered_route(
) )
.await .await
.map_err(|_| { .map_err(|_| {
Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.") err!(BadRequest(
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 {
@@ -62,7 +65,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(Error::BadRequest(ErrorKind::forbidden(), "Room directory is not public")); return Err!(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(
@@ -75,7 +78,10 @@ pub(crate) async fn get_public_rooms_route(
) )
.await .await
.map_err(|_| { .map_err(|_| {
Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.") err!(BadRequest(
ErrorKind::Unknown,
"Failed to return this server's public room list."
))
})?; })?;
Ok(get_public_rooms::v1::Response { Ok(get_public_rooms::v1::Response {
+5 -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::{Error, Result, err}; use conduwuit::{Err, 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,17 +67,16 @@ pub(crate) async fn get_profile_information_route(
.config .config
.allow_inbound_profile_lookup_federation_requests .allow_inbound_profile_lookup_federation_requests
{ {
return Err(Error::BadRequest( return Err!(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(Error::BadRequest( return Err!(
ErrorKind::InvalidParam, BadRequest(ErrorKind::InvalidParam, "User does not belong to this server.",)
"User does not belong to this server.", );
));
} }
let mut displayname = None; let mut displayname = None;
+64 -214
View File
@@ -1,33 +1,27 @@
use std::{ use std::{collections::BTreeMap, net::IpAddr, time::Instant};
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, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
RoomId, ServerName, UserId,
api::{ api::{
client::error::{ErrorKind, ErrorKind::LimitExceeded}, client::error::ErrorKind,
federation::transactions::{ federation::transactions::{
edu::{ edu::{
DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent, DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
@@ -38,16 +32,9 @@ 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;
@@ -57,6 +44,15 @@ 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,
@@ -80,73 +76,16 @@ pub(crate) async fn send_transaction_message_route(
))); )));
} }
let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
// 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 txn_start_time = Instant::now();
trace!(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = %body.transaction_id,
origin = %body.origin(),
"Starting txn",
);
let pdus = body let pdus = body
.pdus .pdus
.iter() .iter()
@@ -163,79 +102,40 @@ async fn process_inbound_transaction(
.filter_map(Result::ok) .filter_map(Result::ok)
.stream(); .stream();
debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",); let results = handle(&services, &client, body.origin(), txn_start_time, pdus, edus).await?;
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(),
"Finished processing transaction" id = %body.transaction_id,
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:?}");
}
}
}
let response = send_transaction_message::v1::Response { Ok(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,
) -> std::result::Result<ResolvedMap, TransactionError> { ) -> Result<ResolvedMap> {
// group pdus by room // group pdus by room
let pdus = pdus let pdus = pdus
.collect() .collect()
@@ -252,7 +152,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, room_id, pdus.into_iter()) handle_room(services, client, origin, started, 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)
}) })
@@ -269,51 +169,14 @@ 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,
) -> std::result::Result<Vec<(OwnedEventId, Result)>, TransactionError> { ) -> Result<Vec<(OwnedEventId, Result)>> {
let _room_lock = services let _room_lock = services
.rooms .rooms
.event_handler .event_handler
@@ -322,40 +185,27 @@ async fn handle_room(
.await; .await;
let room_id = &room_id; let room_id = &room_id;
let pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus pdus.try_stream()
.into_iter() .and_then(|(_, event_id, value)| async move {
.map(|(_, event_id, value)| (event_id, value)) services.server.check_running()?;
.collect(); let pdu_start_time = Instant::now();
// Try to sort PDUs by their dependencies, but fall back to arbitrary order on let result = services
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's .rooms
// responsibility. .event_handler
let sorted_event_ids = if pdu_map.len() >= 2 { .handle_incoming_pdu(origin, room_id, &event_id, value, true)
build_local_dag(&pdu_map).await.unwrap_or_else(|e| { .await
debug_warn!("Failed to build local DAG for room {room_id}: {e}"); .map(|_| ());
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))
}) })
} else { .try_collect()
pdu_map.keys().cloned().collect() .await
};
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) {
@@ -628,8 +478,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
.transactions .transaction_ids
.get_client_txn(sender, None, message_id) .existing_txnid(sender, None, message_id)
.await .await
.is_ok() .is_ok()
{ {
@@ -648,8 +498,8 @@ async fn handle_edu_direct_to_device(
// Save transaction id with empty data // Save transaction id with empty data
services services
.transactions .transaction_ids
.add_client_txnid(sender, None, message_id, &[]); .add_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>(
+6 -7
View File
@@ -1,7 +1,7 @@
use std::time::Duration; use std::time::Duration;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Error, Result}; use conduwuit::{Err, 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(Error::BadRequest( return Err!(BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Tried to access user from other server.", "Tried to access user from other server.",
)); ));
@@ -86,10 +86,9 @@ 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(Error::BadRequest( return Err!(
ErrorKind::InvalidParam, BadRequest(ErrorKind::InvalidParam, "User does not belong to this server.",)
"User does not belong to this server.", );
));
} }
let result = get_keys_helper( let result = get_keys_helper(
@@ -121,7 +120,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(Error::BadRequest( return Err!(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::{Error, Result}; use conduwuit::{Err, Result};
use ruma::api::{client::error::ErrorKind, federation::discovery::discover_homeserver}; use ruma::api::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(Error::BadRequest(ErrorKind::NotFound, "Not found.")), | None => return Err!(BadRequest(ErrorKind::NotFound, "Not found.")),
}, },
}) })
} }
+2 -1
View File
@@ -98,7 +98,8 @@ serde-saphyr.workspace = true
serde.workspace = true serde.workspace = true
smallvec.workspace = true smallvec.workspace = true
smallstr.workspace = true smallstr.workspace = true
thiserror.workspace = true snafu.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
+16 -91
View File
@@ -368,31 +368,6 @@ 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.
/// ///
@@ -678,6 +653,12 @@ 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.
@@ -2068,16 +2049,6 @@ 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,
@@ -2090,12 +2061,6 @@ 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
@@ -2161,16 +2126,17 @@ pub struct WellKnownConfig {
/// listed. /// listed.
pub support_mxid: Option<OwnedUserId>, pub support_mxid: Option<OwnedUserId>,
/// **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
///
/// A list of MatrixRTC foci URLs which will be served as part of the /// A list of MatrixRTC foci URLs which will be served as part of the
/// MSC4143 client endpoint at /.well-known/matrix/client. /// 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" },
/// ]
/// ///
/// This option is deprecated and will be removed in a future release. /// To disable, set this to be an empty vector (`[]`).
/// Please migrate to the new `[global.matrix_rtc]` config section.
/// ///
/// default: [] /// default: []
#[serde(default)] #[serde(default = "default_rtc_focus_urls")]
pub rtc_focus_server_urls: Vec<RtcFocusInfo>, pub rtc_focus_server_urls: Vec<RtcFocusInfo>,
} }
@@ -2199,43 +2165,6 @@ 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 {
@@ -2429,7 +2358,6 @@ 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 {
@@ -2612,12 +2540,6 @@ 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")
@@ -2713,6 +2635,9 @@ 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(),
+129 -30
View File
@@ -45,63 +45,162 @@ 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 {
$crate::ruma::api::client::error::ErrorKind::forbidden(), kind: $crate::ruma::api::client::error::ErrorKind::forbidden(),
$crate::err_log!(buf, $level, $($args)+), message: $crate::err_log!(buf, $level, $($args)+),
$crate::http::StatusCode::BAD_REQUEST code: $crate::http::StatusCode::BAD_REQUEST,
) backtrace: Some($crate::snafu::Backtrace::capture()),
}
}}; }};
(Request(Forbidden($($args:tt)+))) => { (Request(Forbidden($($args:tt)+))) => {
$crate::error::Error::Request( {
$crate::ruma::api::client::error::ErrorKind::forbidden(), let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::format_maybe!($($args)+), $crate::error::Error::Request {
$crate::http::StatusCode::BAD_REQUEST kind: $crate::ruma::api::client::error::ErrorKind::forbidden(),
) 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 {
$crate::ruma::api::client::error::ErrorKind::$variant, kind: $crate::ruma::api::client::error::ErrorKind::$variant,
$crate::err_log!(buf, $level, $($args)+), message: $crate::err_log!(buf, $level, $($args)+),
$crate::http::StatusCode::BAD_REQUEST code: $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( {
$crate::ruma::api::client::error::ErrorKind::$variant, let message: std::borrow::Cow<'static, str> = $crate::format_maybe!($($args)+);
$crate::format_maybe!($($args)+), $crate::error::Error::Request {
$crate::http::StatusCode::BAD_REQUEST kind: $crate::ruma::api::client::error::ErrorKind::$variant,
) 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::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+)) $crate::error::ConfigSnafu {
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::error::Error::$variant($crate::err_log!(buf, $level, $($args)+)) $crate::paste::paste! {
$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::error::Error::$variant($crate::format_maybe!($($args)+)) $crate::paste::paste! {
{
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();
$crate::error::Error::Err($crate::err_log!(buf, $level, $($args)+)) let message: std::borrow::Cow<'static, str> = $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()
}
}; };
} }
@@ -134,7 +233,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)+));
($out).into() std::borrow::Cow::<'static, str>::from($out)
}} }}
} }
+448 -139
View File
@@ -6,151 +6,391 @@ 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(thiserror::Error)] #[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum Error { pub enum Error {
#[error("PANIC!")] #[snafu(display("PANIC!"))]
PanicAny(Box<dyn Any + Send>), PanicAny {
#[error("PANIC! {0}")] panic: Box<dyn Any + Send>,
Panic(&'static str, Box<dyn Any + Send + 'static>), backtrace: snafu::Backtrace,
},
#[snafu(display("PANIC! {message}"))]
Panic {
message: &'static str,
panic: Box<dyn Any + Send + 'static>,
backtrace: snafu::Backtrace,
},
// std // std
#[error(transparent)] #[snafu(display("Format error: {source}"))]
Fmt(#[from] std::fmt::Error), Fmt {
#[error(transparent)] source: std::fmt::Error,
FromUtf8(#[from] std::string::FromUtf8Error), backtrace: snafu::Backtrace,
#[error("I/O error: {0}")] },
Io(#[from] std::io::Error),
#[error(transparent)] #[snafu(display("UTF-8 conversion error: {source}"))]
ParseFloat(#[from] std::num::ParseFloatError), FromUtf8 {
#[error(transparent)] source: std::string::FromUtf8Error,
ParseInt(#[from] std::num::ParseIntError), backtrace: snafu::Backtrace,
#[error(transparent)] },
Std(#[from] Box<dyn std::error::Error + Send>),
#[error(transparent)] #[snafu(display("I/O error: {source}"))]
ThreadAccessError(#[from] std::thread::AccessError), Io {
#[error(transparent)] source: std::io::Error,
TryFromInt(#[from] std::num::TryFromIntError), backtrace: snafu::Backtrace,
#[error(transparent)] },
TryFromSlice(#[from] std::array::TryFromSliceError),
#[error(transparent)] #[snafu(display("Parse float error: {source}"))]
Utf8(#[from] std::str::Utf8Error), ParseFloat {
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
#[error(transparent)] #[snafu(display("Capacity error: {source}"))]
CapacityError(#[from] arrayvec::CapacityError), CapacityError {
#[error(transparent)] source: arrayvec::CapacityError,
CargoToml(#[from] cargo_toml::Error), backtrace: snafu::Backtrace,
#[error(transparent)] },
Clap(#[from] clap::error::Error),
#[error(transparent)] #[snafu(display("Cargo.toml error: {source}"))]
Extension(#[from] axum::extract::rejection::ExtensionRejection), CargoToml {
#[error(transparent)] source: cargo_toml::Error,
Figment(#[from] figment::error::Error), backtrace: snafu::Backtrace,
#[error(transparent)] },
Http(#[from] http::Error),
#[error(transparent)] #[snafu(display("Clap error: {source}"))]
HttpHeader(#[from] http::header::InvalidHeaderValue), Clap {
#[error("Join error: {0}")] source: clap::error::Error,
JoinError(#[from] tokio::task::JoinError), backtrace: snafu::Backtrace,
#[error(transparent)] },
Json(#[from] serde_json::Error),
#[error(transparent)] #[snafu(display("Extension rejection: {source}"))]
JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export Extension {
#[error(transparent)] source: axum::extract::rejection::ExtensionRejection,
JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export backtrace: snafu::Backtrace,
#[error(transparent)] },
Path(#[from] axum::extract::rejection::PathRejection),
#[error("Mutex poisoned: {0}")] #[snafu(display("Figment error: {source}"))]
Poison(Cow<'static, str>), Figment {
#[error("Regex error: {0}")] source: figment::error::Error,
Regex(#[from] regex::Error), backtrace: snafu::Backtrace,
#[error("Request error: {0}")] },
Reqwest(#[from] reqwest::Error),
#[error("{0}")] #[snafu(display("HTTP error: {source}"))]
SerdeDe(Cow<'static, str>), Http {
#[error("{0}")] source: http::Error,
SerdeSer(Cow<'static, str>), backtrace: snafu::Backtrace,
#[error(transparent)] },
TomlDe(#[from] toml::de::Error),
#[error(transparent)] #[snafu(display("Invalid HTTP header value: {source}"))]
TomlSer(#[from] toml::ser::Error), HttpHeader {
#[error("Tracing filter error: {0}")] source: http::header::InvalidHeaderValue,
TracingFilter(#[from] tracing_subscriber::filter::ParseError), backtrace: snafu::Backtrace,
#[error("Tracing reload error: {0}")] },
TracingReload(#[from] tracing_subscriber::reload::Error),
#[error(transparent)] #[snafu(display("Join error: {source}"))]
TypedHeader(#[from] axum_extra::typed_header::TypedHeaderRejection), JoinError {
#[error(transparent)] source: tokio::task::JoinError,
YamlDe(#[from] serde_saphyr::Error), backtrace: snafu::Backtrace,
#[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
#[error("Arithmetic operation failed: {0}")] #[snafu(display("Arithmetic operation failed: {message}"))]
Arithmetic(Cow<'static, str>), Arithmetic {
#[error("{0}: {1}")] message: Cow<'static, str>,
BadRequest(ruma::api::client::error::ErrorKind, &'static str), //TODO: remove backtrace: snafu::Backtrace,
#[error("{0}")] },
BadServerResponse(Cow<'static, str>),
#[error(transparent)] #[snafu(display("{kind}: {message}"))]
CanonicalJson(#[from] ruma::CanonicalJsonError), BadRequest {
#[error("There was a problem with the '{0}' directive in your configuration: {1}")] kind: ruma::api::client::error::ErrorKind,
Config(&'static str, Cow<'static, str>), message: Cow<'static, str>,
#[error("{0}")] backtrace: snafu::Backtrace,
Conflict(Cow<'static, str>), // This is only needed for when a room alias already exists },
#[error(transparent)]
ContentDisposition(#[from] ruma::http_headers::ContentDispositionParseError), #[snafu(display("{message}"))]
#[error("{0}")] BadServerResponse {
Database(Cow<'static, str>), message: Cow<'static, str>,
#[error("Feature '{0}' is not available on this server.")] backtrace: snafu::Backtrace,
FeatureDisabled(Cow<'static, str>), },
#[error("Remote server {0} responded with: {1}")]
Federation(ruma::OwnedServerName, ruma::api::client::error::Error), #[snafu(display("Canonical JSON error: {source}"))]
#[error("{0} in {1}")] CanonicalJson {
InconsistentRoomState(&'static str, ruma::OwnedRoomId), source: ruma::CanonicalJsonError,
#[error(transparent)] backtrace: snafu::Backtrace,
IntoHttp(#[from] ruma::api::error::IntoHttpError), },
#[error("{0}")]
Ldap(Cow<'static, str>), #[snafu(display(
#[error(transparent)] "There was a problem with the '{directive}' directive in your configuration: {message}"
Mxc(#[from] ruma::MxcUriError), ))]
#[error(transparent)] Config {
Mxid(#[from] ruma::IdParseError), directive: &'static str,
#[error("from {0}: {1}")] message: Cow<'static, str>,
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError), backtrace: snafu::Backtrace,
#[error("{0}: {1}")] },
Request(ruma::api::client::error::ErrorKind, Cow<'static, str>, http::StatusCode),
#[error(transparent)] #[snafu(display("{message}"))]
Ruma(#[from] ruma::api::client::error::Error), Conflict {
#[error(transparent)] message: Cow<'static, str>,
Signatures(#[from] ruma::signatures::Error), backtrace: snafu::Backtrace,
#[error(transparent)] },
StateRes(#[from] crate::state_res::Error),
#[error("uiaa")] #[snafu(display("Content disposition error: {source}"))]
Uiaa(ruma::api::client::uiaa::UiaaInfo), ContentDisposition {
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
#[error("{0}")] #[snafu(display("{message}"))]
Err(Cow<'static, str>), Err {
message: Cow<'static, str>,
backtrace: snafu::Backtrace,
},
} }
impl Error { impl Error {
#[inline] #[inline]
#[must_use] #[must_use]
pub fn from_errno() -> Self { Self::Io(std::io::Error::last_os_error()) } pub fn from_errno() -> Self { IoSnafu {}.into_error(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 {
crate::err!(Database(error!("{message}"))) let message: Cow<'static, str> = message.into();
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(),
} }
} }
@@ -158,8 +398,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(origin, error) => format!("Answer from {origin}: {error}"), | Self::Federation { server, error, .. } => format!("Answer from {server}: {error}"),
| Self::Ruma(error) => response::ruma_error_message(error), | Self::Ruma { source, .. } => response::ruma_error_message(source),
| _ => format!("{self}"), | _ => format!("{self}"),
} }
} }
@@ -170,10 +410,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) | Self::Ruma(error) => | Self::Federation { error, .. } => response::ruma_error_kind(error).clone(),
response::ruma_error_kind(error).clone(), | Self::Ruma { source, .. } => response::ruma_error_kind(source).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,
} }
} }
@@ -184,13 +424,15 @@ impl Error {
use http::StatusCode; use http::StatusCode;
match self { match self {
| Self::Federation(_, error) | Self::Ruma(error) => error.status_code, | Self::Federation { error, .. } => error.status_code,
| Self::Request(kind, _, code) => response::status_code(kind, *code), | Self::Ruma { source, .. } => source.status_code,
| Self::BadRequest(kind, ..) => response::bad_request_code(kind), | Self::Request { kind, code, .. } => response::status_code(kind, *code),
| Self::FeatureDisabled(..) => response::bad_request_code(&self.kind()), | Self::BadRequest { kind, .. } => response::bad_request_code(kind),
| Self::Reqwest(error) => error.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), | Self::FeatureDisabled { .. } => response::bad_request_code(&self.kind()),
| Self::Conflict(_) => StatusCode::CONFLICT, | Self::Reqwest { source, .. } =>
| Self::Io(error) => response::io_error_code(error.kind()), source.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
| Self::Conflict { .. } => StatusCode::CONFLICT,
| Self::Io { source, .. } => response::io_error_code(source.kind()),
| _ => StatusCode::INTERNAL_SERVER_ERROR, | _ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
@@ -203,16 +445,46 @@ 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 }
} }
impl std::fmt::Debug for Error { // Debug is already derived by Snafu
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message()) /// Macro to reduce boilerplate for From implementations using Snafu context
} 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 { Self::Poison(e.to_string().into()) } fn from(e: PoisonError<T>) -> Self { PoisonSnafu { message: e.to_string() }.build() }
} }
#[allow(clippy::fallible_impl_from)] #[allow(clippy::fallible_impl_from)]
@@ -224,6 +496,43 @@ 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) {
+8 -5
View File
@@ -15,13 +15,16 @@ impl Error {
#[must_use] #[must_use]
#[inline] #[inline]
pub fn from_panic(e: Box<dyn Any + Send>) -> Self { Self::Panic(debug::panic_str(&e), e) } pub fn from_panic(e: Box<dyn Any + Send>) -> Self {
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(_, e) | Self::PanicAny(e) => e, | Self::Panic { panic, .. } | Self::PanicAny { panic, .. } => panic,
| Self::JoinError(e) => e.into_panic(), | Self::JoinError { source, .. } => source.into_panic(),
| _ => Box::new(self), | _ => Box::new(self),
} }
} }
@@ -37,8 +40,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(e) => e.is_panic(), | Self::JoinError { source, .. } => source.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(uiaainfo) = error { if let Error::Uiaa { info, .. } = error {
return Self::AuthResponse(uiaainfo); return Self::AuthResponse(info);
} }
let body = ErrorBody::Standard { let body = ErrorBody::Standard {
+8 -2
View File
@@ -5,9 +5,15 @@ 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 { Self::SerdeDe(msg.to_string().into()) } fn custom<T: Display + ToString>(msg: T) -> Self {
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 { Self::SerdeSer(msg.to_string().into()) } fn custom<T: Display + ToString>(msg: T) -> Self {
let message: std::borrow::Cow<'static, str> = msg.to_string().into();
super::SerdeSerSnafu { message }.build()
}
} }
-9
View File
@@ -14,7 +14,6 @@ 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]
@@ -22,22 +21,14 @@ 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}"))
+10 -3
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::{Error, Result, err, implement}; use crate::{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,8 +10,15 @@ 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()) redact_content_in_place(&mut content, room_version_id, self.kind.to_string()).map_err(
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?; |error| {
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");
+7 -5
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}, state_res::{self as state_res, Error, Result, StateMap, error::NotFoundSnafu},
}; };
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
@@ -170,10 +170,12 @@ 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 self.0.get(event_id).cloned().ok_or_else(|| {
.get(event_id) NotFoundSnafu {
.cloned() message: format!("{} not found", event_id),
.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
+27 -10
View File
@@ -1,23 +1,40 @@
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use thiserror::Error; use snafu::{IntoError, prelude::*};
/// Represents the various errors that arise when resolving state. /// Represents the various errors that arise when resolving state.
#[derive(Error, Debug)] #[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
#[non_exhaustive] #[non_exhaustive]
pub enum Error { pub enum Error {
/// A deserialization error. /// A deserialization error.
#[error(transparent)] #[snafu(display("JSON error: {source}"))]
SerdeJson(#[from] JsonError), SerdeJson {
source: JsonError,
backtrace: snafu::Backtrace,
},
/// The given option or version is unsupported. /// The given option or version is unsupported.
#[error("Unsupported room version: {0}")] #[snafu(display("Unsupported room version: {version}"))]
Unsupported(String), Unsupported {
version: String,
backtrace: snafu::Backtrace,
},
/// The given event was not found. /// The given event was not found.
#[error("Not found error: {0}")] #[snafu(display("Not found error: {message}"))]
NotFound(String), NotFound {
message: String,
backtrace: snafu::Backtrace,
},
/// Invalid fields in the given PDU. /// Invalid fields in the given PDU.
#[error("Invalid PDU: {0}")] #[snafu(display("Invalid PDU: {message}"))]
InvalidPdu(String), InvalidPdu {
message: String,
backtrace: snafu::Backtrace,
},
}
impl From<serde_json::Error> for Error {
fn from(source: serde_json::Error) -> Self { SerdeJsonSnafu.into_error(source) }
} }
+4 -3
View File
@@ -24,6 +24,7 @@ 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,
@@ -383,8 +384,8 @@ where
return Ok(false); return Ok(false);
} }
let target_user = let target_user = <&UserId>::try_from(state_key)
<&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{e}")))?; .map_err(|e| InvalidPduSnafu { message: format!("{e}") }.build())?;
let user_for_join_auth = content let user_for_join_auth = content
.join_authorised_via_users_server .join_authorised_via_users_server
@@ -461,7 +462,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(Error::InvalidPdu("Missing membership field".to_owned())); return Err(InvalidPduSnafu { message: "Missing membership field" }.build());
}; };
let membership_state = membership_state.deserialize()?; let membership_state = membership_state.deserialize()?;
+41 -29
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; pub(crate) use self::error::{Error, InvalidPduSnafu, NotFoundSnafu};
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, err, debug, debug_error,
matrix::{Event, StateKey},
state_res::room_version::StateResolutionVersion, state_res::room_version::StateResolutionVersion,
trace, trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt}, utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt},
warn, warn,
}; };
@@ -118,7 +118,10 @@ 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(|| {
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned()) InvalidPduSnafu {
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");
@@ -149,10 +152,11 @@ where
let control_events: Vec<_> = all_conflicted let control_events: Vec<_> = all_conflicted
.iter() .iter()
.stream() .stream()
.wide_filter_map(async |id| { .broad_filter_map(async |id| {
is_power_event_id(id, &event_fetch) event_fetch(id.clone())
.await .await
.then_some(id.clone()) .filter(|event| is_power_event(&event))
.map(|_| id.clone())
}) })
.collect() .collect()
.await; .await;
@@ -314,7 +318,10 @@ 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() {
err!("could not fetch event {} to calculate conflicted subgraph", event_id); tracing::error!(
"could not fetch event {} to calculate conflicted subgraph",
event_id
);
path.pop(); path.pop();
continue; continue;
} }
@@ -402,11 +409,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(|| Error::NotFound(String::new()))?; .ok_or_else(|| NotFoundSnafu { message: "" }.build())?;
let ev = fetch_event(event_id) let ev = fetch_event(event_id)
.await .await
.ok_or_else(|| Error::NotFound(String::new()))?; .ok_or_else(|| NotFoundSnafu { message: "" }.build())?;
Ok((pl, ev.origin_server_ts())) Ok((pl, ev.origin_server_ts()))
}; };
@@ -612,9 +619,12 @@ 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()) fetch_event(event_id.to_owned()).await.ok_or_else(|| {
.await NotFoundSnafu {
.ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}"))) message: format!("Failed to find {event_id}"),
}
.build()
})
}) })
.try_collect() .try_collect()
.boxed() .boxed()
@@ -653,7 +663,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(|| Error::InvalidPdu("State event had no state key".to_owned()))?; .ok_or_else(|| InvalidPduSnafu { message: "State event had no state key" }.build())?;
let auth_types = auth_types_for_event( let auth_types = auth_types_for_event(
event.event_type(), event.event_type(),
@@ -669,13 +679,14 @@ 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| {
Error::InvalidPdu(format!( InvalidPduSnafu {
"Failed to parse create event ID from room ID/hash: {e}" message: format!("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() {
@@ -686,7 +697,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(|| {
Error::InvalidPdu("State event had no state key".to_owned()) InvalidPduSnafu { message: "State event had no state key" }.build()
})?), })?),
ev.clone(), ev.clone(),
); );
@@ -801,13 +812,13 @@ where
let event = fetch_event(p.clone()) let event = fetch_event(p.clone())
.await .await
.ok_or_else(|| Error::NotFound(format!("Failed to find {p}")))?; .ok_or_else(|| NotFoundSnafu { message: format!("Failed to find {p}") }.build())?;
pl = None; pl = None;
for aid in event.auth_events() { for aid in event.auth_events() {
let ev = fetch_event(aid.to_owned()) let ev = fetch_event(aid.to_owned()).await.ok_or_else(|| {
.await NotFoundSnafu { message: format!("Failed to find {aid}") }.build()
.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());
@@ -869,9 +880,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()) let aev = fetch_event(aid.to_owned()).await.ok_or_else(|| {
.await NotFoundSnafu { message: format!("Failed to find {aid}") }.build()
.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);
@@ -915,6 +926,7 @@ 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,
+6 -2
View File
@@ -1,6 +1,6 @@
use ruma::RoomVersionId; use ruma::RoomVersionId;
use super::{Error, Result}; use super::{Result, error::UnsupportedSnafu};
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::exhaustive_enums)] #[allow(clippy::exhaustive_enums)]
@@ -163,7 +163,11 @@ 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 => return Err(Error::Unsupported(format!("found version `{ver}`"))), | 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; use super::{auth_types_for_event, error::NotFoundSnafu};
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(|| super::Error::NotFound(format!("{event_id} not found"))) .ok_or_else(|| NotFoundSnafu { message: format!("{event_id} not found") }.build())
.map_err(Into::into) .map_err(Into::into)
} }
+2
View File
@@ -14,9 +14,11 @@ 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;
+9 -7
View File
@@ -3,17 +3,19 @@ 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 with a generic error type /// Convert an Iterator into a TryStream
fn try_stream<E>( fn try_stream(
self, self,
) -> impl TryStream< ) -> impl TryStream<
Ok = <I as IntoIterator>::Item, Ok = <I as IntoIterator>::Item,
Error = E, Error = Error,
Item = Result<<I as IntoIterator>::Item, E>, Item = Result<<I as IntoIterator>::Item, Error>,
> + Send; > + Send;
} }
@@ -26,12 +28,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<E>( fn try_stream(
self, self,
) -> impl TryStream< ) -> impl TryStream<
Ok = <I as IntoIterator>::Item, Ok = <I as IntoIterator>::Item,
Error = E, Error = Error,
Item = Result<<I as IntoIterator>::Item, E>, Item = Result<<I as IntoIterator>::Item, Error>,
> + Send { > + Send {
self.stream().map(Ok) self.stream().map(Ok)
} }
+1 -2
View File
@@ -1,10 +1,9 @@
//! 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
+5 -1
View File
@@ -2,6 +2,8 @@
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;
@@ -142,7 +144,9 @@ pub fn getcpu() -> Result<usize> {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
#[inline] #[inline]
pub fn getcpu() -> Result<usize> { Err(crate::Error::Io(std::io::ErrorKind::Unsupported.into())) } pub fn getcpu() -> Result<usize> {
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()
+12 -7
View File
@@ -255,7 +255,10 @@ 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| Self::Error::SerdeDe(e.to_string().into())), .map_err(|e| {
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),
} }
@@ -313,9 +316,10 @@ 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 let bytes = bytes.into_inner().map_err(|_| {
.into_inner() let message: std::borrow::Cow<'static, str> = "i64 buffer underflow".into();
.map_err(|_| Self::Error::SerdeDe("i64 buffer underflow".into()))?; conduwuit_core::error::SerdeDeSnafu { message }.build()
})?;
self.inc_pos(BYTES); self.inc_pos(BYTES);
visitor.visit_i64(i64::from_be_bytes(bytes)) visitor.visit_i64(i64::from_be_bytes(bytes))
@@ -345,9 +349,10 @@ 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 let bytes = bytes.into_inner().map_err(|_| {
.into_inner() let message: std::borrow::Cow<'static, str> = "u64 buffer underflow".into();
.map_err(|_| Self::Error::SerdeDe("u64 buffer underflow".into()))?; conduwuit_core::error::SerdeDeSnafu { message }.build()
})?;
self.inc_pos(BYTES); self.inc_pos(BYTES);
visitor.visit_u64(u64::from_be_bytes(bytes)) visitor.visit_u64(u64::from_be_bytes(bytes))
+4 -1
View File
@@ -199,7 +199,10 @@ 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| Self::Error::SerdeSer(e.to_string().into())) .map_err(|e| {
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:?}"),
} }
+7 -2
View File
@@ -1,4 +1,4 @@
use std::sync::Arc; use std::{borrow::Cow, sync::Arc};
use axum::{Router, response::IntoResponse}; use axum::{Router, response::IntoResponse};
use conduwuit::Error; use conduwuit::Error;
@@ -18,5 +18,10 @@ 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(ErrorKind::Unrecognized, "Not Found".into(), StatusCode::NOT_FOUND) Error::Request {
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(err!(Request(InvalidParam( return 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(err!(Request(InvalidParam( return 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_media().to_owned()); .unwrap_or_else(|| conduwuit::version::user_agent().to_owned());
Ok(Arc::new(Self { Ok(Arc::new(Self {
default: base(config)? default: base(config)?
+2 -5
View File
@@ -2,7 +2,7 @@ use std::{fmt::Debug, mem};
use bytes::Bytes; use bytes::Bytes;
use conduwuit::{ use conduwuit::{
Err, Error, Result, debug, debug::INFO_SPAN_LEVEL, debug_error, debug_warn, err, Err, 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,10 +179,7 @@ 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(Error::Federation( return Err!(Federation(dest.to_owned(), RumaError::from_http_response(http_response),));
dest.to_owned(),
RumaError::from_http_response(http_response),
));
} }
Ok(http_response) Ok(http_response)
+9 -11
View File
@@ -67,17 +67,15 @@ 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, provided it's not // first run mode will be enabled if there are no local users
// forcibly disabled for Complement tests let is_first_run = self
let is_first_run = !self.services.config.force_disable_first_run_mode .services
&& self .users
.services .list_local_users()
.users .ready_filter(|user| *user != self.services.globals.server_user)
.list_local_users() .next()
.ready_filter(|user| *user != self.services.globals.server_user) .await
.next() .is_none();
.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,8 +170,6 @@ 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,9 +37,6 @@ 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(NotFound, ..)) = &result { if let Err(Error::Request { kind: 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(Unrecognized, ..)) = &result { if let Err(Error::Request { kind: 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 transactions; pub mod transaction_ids;
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::<conduwuit::Error>() .try_stream()
.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,9 +63,7 @@ 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,9 +162,7 @@ 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,7 +112,14 @@ 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);
state_res::resolve(room_version, state_sets, auth_chain_sets, &event_fetch, &event_exists) Ok(
.map_err(|e| err!(error!("State resolution failed: {e:?}"))) state_res::resolve(
.await room_version,
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::{Error, Result}; use conduwuit::{Err, 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(Error::BadRequest(ErrorKind::InvalidParam, "invalid token")) Err!(BadRequest(ErrorKind::InvalidParam, "invalid token"))
} }
} }
} }
+4 -5
View File
@@ -75,10 +75,7 @@ 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(Error::InconsistentRoomState( Err!(InconsistentRoomState("non-create event for room of unknown version", room_id))
"non-create event for room of unknown version",
room_id,
))
} }
} }
let PduBuilder { let PduBuilder {
@@ -275,7 +272,9 @@ 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(ruma::signatures::Error::PduSize) => { | Error::Signatures { source, .. }
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, transactions, uiaa, users, sync, transaction_ids, 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 transactions: Arc<transactions::Service>, pub transaction_ids: Arc<transaction_ids::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),
transactions: build!(transactions::Service), transaction_ids: build!(transaction_ids::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
@@ -0,0 +1,54 @@
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
@@ -1,326 +0,0 @@
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, Error, Result, SyncRwLock, err, error, implement, utils, Err, 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(Error::BadRequest( return Err!(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(Error::BadRequest( return Err!(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(|_| Error::BadRequest(ErrorKind::InvalidParam, "User ID is invalid."))?; .map_err(|_| err!(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(Error::BadRequest( let self_signing_key_id = self_signing_key_ids.next().ok_or(err!(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(Error::BadRequest( return Err!(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(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid master key"))?; .map_err(|_| err!(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(Error::BadRequest(ErrorKind::InvalidParam, "Master key contained no key."))?; .ok_or(err!(BadRequest(ErrorKind::InvalidParam, "Master key contained no key.")))?;
if master_key_ids.next().is_some() { if master_key_ids.next().is_some() {
return Err(Error::BadRequest( return Err!(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
thiserror.workspace = true snafu.workspace = true
[lints] [lints]
workspace = true workspace = true
+12 -4
View File
@@ -8,6 +8,7 @@ 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()
@@ -48,10 +49,17 @@ async fn logo_handler() -> impl IntoResponse {
) )
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, Snafu)]
enum WebError { enum WebError {
#[error("Failed to render template: {0}")] #[snafu(display("Failed to render template: {source}"))]
Render(#[from] askama::Error), Render {
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 {
@@ -66,7 +74,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(err) -%} {% when WebError::Render { source, .. } -%}
<pre>{{ err }}</pre> <pre>{{ source }}</pre>
{% else -%} <p>An error occurred</p> {% else -%} <p>An error occurred</p>
{%- endmatch -%} {%- endmatch -%}
+37 -125
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":"fail","Test":"TestArchivedRoomsHistory/timeline_is_empty"} {"Action":"pass","Test":"TestArchivedRoomsHistory/timeline_is_empty"}
{"Action":"skip","Test":"TestArchivedRoomsHistory/timeline_is_empty/incremental_sync"} {"Action":"skip","Test":"TestArchivedRoomsHistory/timeline_is_empty/incremental_sync"}
{"Action":"fail","Test":"TestArchivedRoomsHistory/timeline_is_empty/initial_sync"} {"Action":"pass","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,11 +79,9 @@
{"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"}
@@ -91,18 +89,19 @@
{"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":"pass","Test":"TestDelayedEvents/cannot_update_a_delayed_event_with_an_invalid_action"} {"Action":"fail","Test":"TestDelayedEvents/cannot_update_a_delayed_event_with_an_invalid_action"}
{"Action":"pass","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_an_action"} {"Action":"pass","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_a_delay_ID"}
{"Action":"fail","Test":"TestDelayedEvents/delayed_event_lookups_are_authenticated"} {"Action":"fail","Test":"TestDelayedEvents/cannot_update_a_delayed_event_without_a_request_body"}
{"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"}
@@ -154,14 +153,12 @@
{"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":"fail","Test":"TestFederationRoomsInvite"} {"Action":"pass","Test":"TestFederationRoomsInvite"}
{"Action":"fail","Test":"TestFederationRoomsInvite/Parallel"} {"Action":"pass","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"}
@@ -194,18 +191,6 @@
{"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"}
@@ -229,20 +214,18 @@
{"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_timestamp"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_after_given_timestmap"}
{"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_before_given_timestamp"} {"Action":"fail","Test":"TestJumpToDateEndpoint/parallel/should_find_event_before_given_timestmap"}
{"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_after_given_timestmap_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_timestamp"} {"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_after_the_latest_timestmap"}
{"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_before_the_earliest_timestamp"} {"Action":"pass","Test":"TestJumpToDateEndpoint/parallel/should_find_nothing_before_the_earliest_timestmap"}
{"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"}
@@ -269,8 +252,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":"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"}
{"Action":"fail","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"} {"Action":"pass","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"}
@@ -295,8 +278,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":"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"}
{"Action":"fail","Test":"TestKnockingInMSC3787Room/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"} {"Action":"pass","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"}
@@ -325,43 +308,10 @@
{"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":"fail","Test":"TestMediaFilenames"} {"Action":"pass","Test":"TestMediaFilenames"}
{"Action":"fail","Test":"TestMediaFilenames/Parallel"} {"Action":"pass","Test":"TestMediaFilenames/Parallel"}
{"Action":"fail","Test":"TestMediaFilenames/Parallel/ASCII"} {"Action":"pass","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'"}
@@ -369,11 +319,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":"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_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":"fail","Test":"TestMediaFilenames/Parallel/Unicode"} {"Action":"pass","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":"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_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"}
@@ -402,15 +352,9 @@
{"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":"TestMessagesOverFederation"} {"Action":"fail","Test":"TestNetworkPartitionOrdering"}
{"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":"fail","Test":"TestOlderLeftRoomsNotInLeaveSection"} {"Action":"pass","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"}
@@ -435,23 +379,7 @@
{"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"}
@@ -512,16 +440,14 @@
{"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":"pass","Test":"TestRestrictedRoomsRemoteJoinFailOver"} {"Action":"fail","Test":"TestRestrictedRoomsRemoteJoinFailOver"}
{"Action":"pass","Test":"TestRestrictedRoomsRemoteJoinFailOverInMSC3787Room"} {"Action":"fail","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"}
@@ -549,19 +475,16 @@
{"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":"fail","Test":"TestRoomCreate"} {"Action":"pass","Test":"TestRoomCreate"}
{"Action":"fail","Test":"TestRoomCreate/Parallel"} {"Action":"pass","Test":"TestRoomCreate/Parallel"}
{"Action":"pass","Test":"TestRoomCreate/Parallel/Can_/sync_newly_created_room"} {"Action":"pass","Test":"TestRoomCreate/Parallel/Can_/sync_newly_created_room"}
{"Action":"fail","Test":"TestRoomCreate/Parallel/POST_/createRoom_creates_a_room_with_the_given_version"} {"Action":"pass","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)"}
@@ -604,7 +527,6 @@
{"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"}
@@ -653,7 +575,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":"fail","Test":"TestSendJoinPartialStateResponse"} {"Action":"skip","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"}
@@ -667,7 +589,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":"fail","Test":"TestSync/parallel/sync_should_succeed_even_if_the_sync_token_points_to_a_redaction_of_an_unknown_event"} {"Action":"pass","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"}
@@ -681,21 +603,12 @@
{"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":"fail","Test":"TestToDeviceMessagesOverFederation/interrupted_connectivity"} {"Action":"pass","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"}
@@ -704,7 +617,6 @@
{"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"}
@@ -712,7 +624,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":"pass","Test":"TestUnrejectRejectedEvents"} {"Action":"fail","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"}
@@ -725,7 +637,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":"fail","Test":"TestUserAppearsInChangedDeviceListOnJoinOverFederation"} {"Action":"pass","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"}