Compare commits

..

6 Commits

Author SHA1 Message Date
Jade Ellis c40cc3b236 chore: Release 2026-03-03 20:59:08 +00:00
Jade Ellis 754959e80d fix: Don't process admin escape commands for local users from federation
Reviewed-By: timedout <git@nexy7574.co.uk>
2026-03-03 19:55:50 +00:00
timedout 37888fb670 fix: Limit body read size of remote requests (CWE-409)
Reviewed-By: Jade Ellis <jade@ellis.link>
2026-03-03 19:54:34 +00:00
Jade Ellis 7207398a9e docs: Changelog 2026-03-03 19:39:54 +00:00
Jason Volk 1a7bda209b feat: Implement Dehydrated Devices MSC3814
Co-authored-by: Jade Ellis <jade@ellis.link>
Signed-off-by: Jason Volk <jason@zemos.net>
2026-03-03 19:39:53 +00:00
Autumn Ashton 7e1950b3d2 fix(docker): Fix building a docker container with dev profile
In Rust, the dev profile uses "debug" as the name of the output folder.
2026-03-03 19:31:04 +00:00
34 changed files with 619 additions and 236 deletions
Generated
+26 -11
View File
@@ -887,7 +887,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit" name = "conduwuit"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"clap", "clap",
"conduwuit_admin", "conduwuit_admin",
@@ -919,7 +919,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_admin" name = "conduwuit_admin"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"clap", "clap",
"conduwuit_api", "conduwuit_api",
@@ -940,7 +940,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_api" name = "conduwuit_api"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -972,14 +972,14 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_build_metadata" name = "conduwuit_build_metadata"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"built", "built",
] ]
[[package]] [[package]]
name = "conduwuit_core" name = "conduwuit_core"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"argon2", "argon2",
"arrayvec", "arrayvec",
@@ -1041,7 +1041,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_database" name = "conduwuit_database"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"conduwuit_core", "conduwuit_core",
@@ -1059,7 +1059,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_macros" name = "conduwuit_macros"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
@@ -1069,7 +1069,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_router" name = "conduwuit_router"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"axum", "axum",
"axum-client-ip", "axum-client-ip",
@@ -1103,7 +1103,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_service" name = "conduwuit_service"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"askama", "askama",
"async-trait", "async-trait",
@@ -1145,7 +1145,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_web" name = "conduwuit_web"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"askama", "askama",
"axum", "axum",
@@ -4055,12 +4055,14 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-util",
"tower", "tower",
"tower-http", "tower-http",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots",
] ]
@@ -5868,6 +5870,19 @@ dependencies = [
"wasmparser", "wasmparser",
] ]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "wasmparser" name = "wasmparser"
version = "0.244.0" version = "0.244.0"
@@ -6332,7 +6347,7 @@ dependencies = [
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.5.5" version = "0.5.6-alpha"
dependencies = [ dependencies = [
"askama", "askama",
"cargo_metadata", "cargo_metadata",
+3 -1
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml` # See also `rust-toolchain.toml`
readme = "README.md" readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity" repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.5" version = "0.5.6-alpha"
[workspace.metadata.crane] [workspace.metadata.crane]
name = "conduwuit" name = "conduwuit"
@@ -144,6 +144,7 @@ features = [
"socks", "socks",
"hickory-dns", "hickory-dns",
"http2", "http2",
"stream",
] ]
[workspace.dependencies.serde] [workspace.dependencies.serde]
@@ -363,6 +364,7 @@ features = [
"unstable-msc2870", "unstable-msc2870",
"unstable-msc3026", "unstable-msc3026",
"unstable-msc3061", "unstable-msc3061",
"unstable-msc3814",
"unstable-msc3245", "unstable-msc3245",
"unstable-msc3266", "unstable-msc3266",
"unstable-msc3381", # polls "unstable-msc3381", # polls
+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
@@ -0,0 +1 @@
Added MSC3814 Dehydrated Devices - you can now decrypt messages sent while all devices were logged out.
+15 -3
View File
@@ -15,6 +15,18 @@ disallowed-macros = [
{ path = "log::trace", reason = "use conduwuit_core::trace" }, { path = "log::trace", reason = "use conduwuit_core::trace" },
] ]
disallowed-methods = [ [[disallowed-methods]]
{ path = "tokio::spawn", reason = "use and pass conduuwit_core::server::Server::runtime() to spawn from" }, path = "tokio::spawn"
] reason = "use and pass conduwuit_core::server::Server::runtime() to spawn from"
[[disallowed-methods]]
path = "reqwest::Response::bytes"
reason = "bytes is unsafe, use limit_read via the conduwuit_core::utils::LimitReadExt trait instead"
[[disallowed-methods]]
path = "reqwest::Response::text"
reason = "text is unsafe, use limit_read_text via the conduwuit_core::utils::LimitReadExt trait instead"
[[disallowed-methods]]
path = "reqwest::Response::json"
reason = "json is unsafe, use limit_read_text via the conduwuit_core::utils::LimitReadExt trait instead"
+2 -3
View File
@@ -11,7 +11,7 @@ allow_guest_registration = true
allow_public_room_directory_over_federation = true allow_public_room_directory_over_federation = 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 +24,7 @@ url_preview_domain_explicit_denylist = ["*"]
media_compat_file_link = false media_compat_file_link = false
media_startup_check = true media_startup_check = true
prune_missing_media = true prune_missing_media = true
log_colors = 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 +47,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
+7 -2
View File
@@ -180,6 +180,11 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
export RUSTFLAGS="${RUSTFLAGS}" export RUSTFLAGS="${RUSTFLAGS}"
fi fi
RUST_PROFILE_DIR="${RUST_PROFILE}"
if [[ "${RUST_PROFILE}" == "dev" ]]; then
RUST_PROFILE_DIR="debug"
fi
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \ TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
jq -r ".target_directory")) jq -r ".target_directory"))
mkdir /out/sbin mkdir /out/sbin
@@ -191,8 +196,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name")) jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
for BINARY in "${BINARIES[@]}"; do for BINARY in "${BINARIES[@]}"; do
echo $BINARY echo $BINARY
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY /out/sbin/$BINARY cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY /out/sbin/$BINARY
done done
EOF EOF
+10 -2
View File
@@ -1,6 +1,6 @@
use std::fmt::Write; use std::fmt::Write;
use conduwuit::{Err, Result}; use conduwuit::{Err, Result, utils::response::LimitReadExt};
use futures::StreamExt; use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId}; use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId};
@@ -55,7 +55,15 @@ pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName
.send() .send()
.await?; .await?;
let text = response.text().await?; let text = response
.limit_read_text(
self.services
.config
.max_request_size
.try_into()
.expect("u64 fits into usize"),
)
.await?;
if text.is_empty() { if text.is_empty() {
return Err!("Response text/body is empty."); return Err!("Response text/body is empty.");
+121
View File
@@ -0,0 +1,121 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, at};
use futures::StreamExt;
use ruma::api::client::dehydrated_device::{
delete_dehydrated_device::unstable as delete_dehydrated_device,
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
put_dehydrated_device::unstable as put_dehydrated_device,
};
use crate::Ruma;
const MAX_BATCH_EVENTS: usize = 50;
/// # `PUT /_matrix/client/../dehydrated_device`
///
/// Creates or overwrites the user's dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn put_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let sender_user = body
.sender_user
.as_deref()
.expect("AccessToken authentication required");
let device_id = body.body.device_id.clone();
services
.users
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response { device_id })
}
/// # `DELETE /_matrix/client/../dehydrated_device`
///
/// Deletes the user's dehydrated device without replacement.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn delete_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device_id = services.users.get_dehydrated_device_id(sender_user).await?;
services.users.remove_device(sender_user, &device_id).await;
Ok(delete_dehydrated_device::Response { device_id })
}
/// # `GET /_matrix/client/../dehydrated_device`
///
/// Gets the user's dehydrated device
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device = services.users.get_dehydrated_device(sender_user).await?;
Ok(get_dehydrated_device::Response {
device_id: device.device_id,
device_data: device.device_data,
})
}
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
///
/// Paginates the events of the dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_events_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.sender_user();
let device_id = &body.body.device_id;
let existing_id = services.users.get_dehydrated_device_id(sender_user).await;
if existing_id.as_ref().is_err()
|| existing_id
.as_ref()
.is_ok_and(|existing_id| existing_id != device_id)
{
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
}
let since: Option<u64> = body
.body
.next_batch
.as_deref()
.map(str::parse)
.transpose()?;
let mut next_batch: Option<u64> = None;
let events = services
.users
.get_to_device_events(sender_user, device_id, since, None)
.take(MAX_BATCH_EVENTS)
.inspect(|&(count, _)| {
next_batch.replace(count);
})
.map(at!(1))
.collect()
.await;
Ok(get_events::Response {
events,
next_batch: next_batch.as_ref().map(ToString::to_string),
})
}
+2
View File
@@ -6,6 +6,7 @@ pub(super) mod appservice;
pub(super) mod backup; pub(super) mod backup;
pub(super) mod capabilities; pub(super) mod capabilities;
pub(super) mod context; pub(super) mod context;
pub(super) mod dehydrated_device;
pub(super) mod device; pub(super) mod device;
pub(super) mod directory; pub(super) mod directory;
pub(super) mod filter; pub(super) mod filter;
@@ -49,6 +50,7 @@ pub(super) use appservice::*;
pub(super) use backup::*; pub(super) use backup::*;
pub(super) use capabilities::*; pub(super) use capabilities::*;
pub(super) use context::*; pub(super) use context::*;
pub(super) use dehydrated_device::*;
pub(super) use device::*; pub(super) use device::*;
pub(super) use directory::*; pub(super) use directory::*;
pub(super) use filter::*; pub(super) use filter::*;
+2 -1
View File
@@ -11,7 +11,7 @@ use std::{
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Result, extract_variant, Result, at, extract_variant,
utils::{ utils::{
ReadyExt, TryFutureExtExt, ReadyExt, TryFutureExtExt,
stream::{BroadbandExt, Tools, WidebandExt}, stream::{BroadbandExt, Tools, WidebandExt},
@@ -385,6 +385,7 @@ pub(crate) async fn build_sync_events(
last_sync_end_count, last_sync_end_count,
Some(current_count), Some(current_count),
) )
.map(at!(1))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let device_one_time_keys_count = services let device_one_time_keys_count = services
+1
View File
@@ -1029,6 +1029,7 @@ async fn collect_to_device(
events: services events: services
.users .users
.get_to_device_events(sender_user, sender_device, None, Some(next_batch)) .get_to_device_events(sender_user, sender_device, None, Some(next_batch))
.map(at!(1))
.collect() .collect()
.await, .await,
}) })
+1
View File
@@ -50,6 +50,7 @@ pub(crate) async fn get_supported_versions_route(
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */ ("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */ ("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */ ("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */ ("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */ ("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */ ("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
+4
View File
@@ -160,6 +160,10 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::update_device_route) .ruma_route(&client::update_device_route)
.ruma_route(&client::delete_device_route) .ruma_route(&client::delete_device_route)
.ruma_route(&client::delete_devices_route) .ruma_route(&client::delete_devices_route)
.ruma_route(&client::put_dehydrated_device_route)
.ruma_route(&client::delete_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_events_route)
.ruma_route(&client::get_tags_route) .ruma_route(&client::get_tags_route)
.ruma_route(&client::update_tag_route) .ruma_route(&client::update_tag_route)
.ruma_route(&client::delete_tag_route) .ruma_route(&client::delete_tag_route)
-10
View File
@@ -2068,16 +2068,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,
+1
View File
@@ -11,6 +11,7 @@ pub mod json;
pub mod math; pub mod math;
pub mod mutex_map; pub mod mutex_map;
pub mod rand; pub mod rand;
pub mod response;
pub mod result; pub mod result;
pub mod set; pub mod set;
pub mod stream; pub mod stream;
+51
View File
@@ -0,0 +1,51 @@
use futures::StreamExt;
use num_traits::ToPrimitive;
use crate::Err;
/// Reads the response body while enforcing a maximum size limit to prevent
/// memory exhaustion.
pub async fn limit_read(response: reqwest::Response, max_size: u64) -> crate::Result<Vec<u8>> {
if response.content_length().is_some_and(|len| len > max_size) {
return Err!(BadServerResponse("Response too large"));
}
let mut data = Vec::new();
let mut reader = response.bytes_stream();
while let Some(chunk) = reader.next().await {
let chunk = chunk?;
data.extend_from_slice(&chunk);
if data.len() > max_size.to_usize().expect("max_size must fit in usize") {
return Err!(BadServerResponse("Response too large"));
}
}
Ok(data)
}
/// Reads the response body as text while enforcing a maximum size limit to
/// prevent memory exhaustion.
pub async fn limit_read_text(
response: reqwest::Response,
max_size: u64,
) -> crate::Result<String> {
let text = String::from_utf8(limit_read(response, max_size).await?)?;
Ok(text)
}
#[allow(async_fn_in_trait)]
pub trait LimitReadExt {
async fn limit_read(self, max_size: u64) -> crate::Result<Vec<u8>>;
async fn limit_read_text(self, max_size: u64) -> crate::Result<String>;
}
impl LimitReadExt for reqwest::Response {
async fn limit_read(self, max_size: u64) -> crate::Result<Vec<u8>> {
limit_read(self, max_size).await
}
async fn limit_read_text(self, max_size: u64) -> crate::Result<String> {
limit_read_text(self, max_size).await
}
}
+4
View File
@@ -362,6 +362,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userid_blurhash", name: "userid_blurhash",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "userid_dehydrateddevice",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "userid_devicelistversion", name: "userid_devicelistversion",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
+15 -1
View File
@@ -530,7 +530,12 @@ impl Service {
Ok(()) Ok(())
} }
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> Option<InvocationSource> pub async fn is_admin_command<E>(
&self,
event: &E,
body: &str,
sent_locally: bool,
) -> Option<InvocationSource>
where where
E: Event + Send + Sync, E: Event + Send + Sync,
{ {
@@ -580,6 +585,15 @@ impl Service {
return None; return None;
} }
// Escaped commands must be sent locally (via client API), not via federation
if !sent_locally {
conduwuit::warn!(
"Ignoring escaped admin command from {} that arrived via federation",
event.sender()
);
return None;
}
// Looks good // Looks good
Some(InvocationSource::EscapedCommand) Some(InvocationSource::EscapedCommand)
} }
+2 -2
View File
@@ -18,7 +18,7 @@
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use async_trait::async_trait; use async_trait::async_trait;
use conduwuit::{Result, Server, debug, error, warn}; use conduwuit::{Result, Server, debug, error, utils::response::LimitReadExt, warn};
use database::{Deserialized, Map}; use database::{Deserialized, Map};
use ruma::events::{Mentions, room::message::RoomMessageEventContent}; use ruma::events::{Mentions, room::message::RoomMessageEventContent};
use serde::Deserialize; use serde::Deserialize;
@@ -137,7 +137,7 @@ impl Service {
.get(CHECK_FOR_ANNOUNCEMENTS_URL) .get(CHECK_FOR_ANNOUNCEMENTS_URL)
.send() .send()
.await? .await?
.text() .limit_read_text(1024 * 1024)
.await?; .await?;
let response = serde_json::from_str::<CheckForAnnouncementsResponse>(&response)?; let response = serde_json::from_str::<CheckForAnnouncementsResponse>(&response)?;
+26 -10
View File
@@ -2,8 +2,8 @@ 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, Error, Result, debug, debug::INFO_SPAN_LEVEL, debug_error, debug_warn, err, implement,
error::inspect_debug_log, implement, trace, trace, utils::response::LimitReadExt,
}; };
use http::{HeaderValue, header::AUTHORIZATION}; use http::{HeaderValue, header::AUTHORIZATION};
use ipaddress::IPAddress; use ipaddress::IPAddress;
@@ -133,7 +133,22 @@ async fn handle_response<T>(
where where
T: OutgoingRequest + Send, T: OutgoingRequest + Send,
{ {
let response = into_http_response(dest, actual, method, url, response).await?; const HUGE_ENDPOINTS: [&str; 2] =
["/_matrix/federation/v2/send_join/", "/_matrix/federation/v2/state/"];
let size_limit: u64 = if HUGE_ENDPOINTS.iter().any(|e| url.path().starts_with(e)) {
// Some federation endpoints can return huge response bodies, so we'll bump the
// limit for those endpoints specifically.
self.services
.server
.config
.max_request_size
.saturating_mul(10)
} else {
self.services.server.config.max_request_size
}
.try_into()
.expect("size_limit (usize) should fit within a u64");
let response = into_http_response(dest, actual, method, url, response, size_limit).await?;
T::IncomingResponse::try_from_http_response(response) T::IncomingResponse::try_from_http_response(response)
.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}"))) .map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}")))
@@ -145,6 +160,7 @@ async fn into_http_response(
method: &Method, method: &Method,
url: &Url, url: &Url,
mut response: Response, mut response: Response,
max_size: u64,
) -> Result<http::Response<Bytes>> { ) -> Result<http::Response<Bytes>> {
let status = response.status(); let status = response.status();
trace!( trace!(
@@ -167,14 +183,14 @@ async fn into_http_response(
); );
trace!("Waiting for response body..."); trace!("Waiting for response body...");
let body = response
.bytes()
.await
.inspect_err(inspect_debug_log)
.unwrap_or_else(|_| Vec::new().into());
let http_response = http_response_builder let http_response = http_response_builder
.body(body) .body(
response
.limit_read(max_size)
.await
.unwrap_or_default()
.into(),
)
.expect("reqwest body is valid http body"); .expect("reqwest body is valid http body");
debug!("Got {status:?} for {method} {url}"); debug!("Got {status:?} for {method} {url}");
+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 {
+31 -21
View File
@@ -7,7 +7,7 @@
use std::time::SystemTime; use std::time::SystemTime;
use conduwuit::{Err, Result, debug, err}; use conduwuit::{Err, Result, debug, err, utils::response::LimitReadExt};
use conduwuit_core::implement; use conduwuit_core::implement;
use ipaddress::IPAddress; use ipaddress::IPAddress;
use serde::Serialize; use serde::Serialize;
@@ -112,8 +112,22 @@ pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
use image::ImageReader; use image::ImageReader;
use ruma::Mxc; use ruma::Mxc;
let image = self.services.client.url_preview.get(url).send().await?; let image = self
let image = image.bytes().await?; .services
.client
.url_preview
.get(url)
.send()
.await?
.limit_read(
self.services
.server
.config
.max_request_size
.try_into()
.expect("u64 should fit in usize"),
)
.await?;
let mxc = Mxc { let mxc = Mxc {
server_name: self.services.globals.server_name(), server_name: self.services.globals.server_name(),
media_id: &random_string(super::MXC_LENGTH), media_id: &random_string(super::MXC_LENGTH),
@@ -151,24 +165,20 @@ async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
use webpage::HTML; use webpage::HTML;
let client = &self.services.client.url_preview; let client = &self.services.client.url_preview;
let mut response = client.get(url).send().await?; let body = client
.get(url)
let mut bytes: Vec<u8> = Vec::new(); .send()
while let Some(chunk) = response.chunk().await? { .await?
bytes.extend_from_slice(&chunk); .limit_read_text(
if bytes.len() > self.services.globals.url_preview_max_spider_size() { self.services
debug!( .server
"Response body from URL {} exceeds url_preview_max_spider_size ({}), not \ .config
processing the rest of the response body and assuming our necessary data is in \ .max_request_size
this range.", .try_into()
url, .expect("u64 should fit in usize"),
self.services.globals.url_preview_max_spider_size() )
); .await?;
break; let Ok(html) = HTML::from_string(body.clone(), Some(url.to_owned())) else {
}
}
let body = String::from_utf8_lossy(&bytes);
let Ok(html) = HTML::from_string(body.to_string(), Some(url.to_owned())) else {
return Err!(Request(Unknown("Failed to parse HTML"))); return Err!(Request(Unknown("Failed to parse HTML")));
}; };
+9 -4
View File
@@ -2,7 +2,7 @@ use std::{fmt::Debug, time::Duration};
use conduwuit::{ use conduwuit::{
Err, Error, Result, debug_warn, err, implement, Err, Error, Result, debug_warn, err, implement,
utils::content_disposition::make_content_disposition, utils::{content_disposition::make_content_disposition, response::LimitReadExt},
}; };
use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE, HeaderValue}; use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE, HeaderValue};
use ruma::{ use ruma::{
@@ -286,10 +286,15 @@ async fn location_request(&self, location: &str) -> Result<FileMeta> {
.and_then(Result::ok); .and_then(Result::ok);
response response
.bytes() .limit_read(
self.services
.server
.config
.max_request_size
.try_into()
.expect("u64 should fit in usize"),
)
.await .await
.map(Vec::from)
.map_err(Into::into)
.map(|content| FileMeta { .map(|content| FileMeta {
content: Some(content), content: Some(content),
content_type: content_type.clone(), content_type: content_type.clone(),
+13 -2
View File
@@ -1,6 +1,7 @@
use std::{fmt::Debug, mem, sync::Arc}; use std::{fmt::Debug, mem, sync::Arc};
use bytes::BytesMut; use bytes::BytesMut;
use conduwuit::utils::response::LimitReadExt;
use conduwuit_core::{ use conduwuit_core::{
Err, Event, Result, debug_warn, err, trace, Err, Event, Result, debug_warn, err, trace,
utils::{stream::TryIgnore, string_from_bytes}, utils::{stream::TryIgnore, string_from_bytes},
@@ -30,7 +31,7 @@ use ruma::{
uint, uint,
}; };
use crate::{Dep, client, globals, rooms, sending, users}; use crate::{Dep, client, config, globals, rooms, sending, users};
pub struct Service { pub struct Service {
db: Data, db: Data,
@@ -39,6 +40,7 @@ pub struct Service {
struct Services { struct Services {
globals: Dep<globals::Service>, globals: Dep<globals::Service>,
config: Dep<config::Service>,
client: Dep<client::Service>, client: Dep<client::Service>,
state_accessor: Dep<rooms::state_accessor::Service>, state_accessor: Dep<rooms::state_accessor::Service>,
state_cache: Dep<rooms::state_cache::Service>, state_cache: Dep<rooms::state_cache::Service>,
@@ -61,6 +63,7 @@ impl crate::Service for Service {
services: Services { services: Services {
globals: args.depend::<globals::Service>("globals"), globals: args.depend::<globals::Service>("globals"),
client: args.depend::<client::Service>("client"), client: args.depend::<client::Service>("client"),
config: args.depend::<config::Service>("config"),
state_accessor: args state_accessor: args
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"), .depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"), state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
@@ -245,7 +248,15 @@ impl Service {
.expect("http::response::Builder is usable"), .expect("http::response::Builder is usable"),
); );
let body = response.bytes().await?; let body = response
.limit_read(
self.services
.config
.max_request_size
.try_into()
.expect("usize fits into u64"),
)
.await?;
if !status.is_success() { if !status.is_success() {
debug_warn!("Push gateway response body: {:?}", string_from_bytes(&body)); debug_warn!("Push gateway response body: {:?}", string_from_bytes(&body));
+4 -6
View File
@@ -1,4 +1,6 @@
use conduwuit::{Result, debug, debug_error, debug_info, debug_warn, implement, trace}; use conduwuit::{
Result, debug, debug_error, debug_info, implement, trace, utils::response::LimitReadExt,
};
#[implement(super::Service)] #[implement(super::Service)]
#[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))] #[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))]
@@ -24,12 +26,8 @@ pub(super) async fn request_well_known(&self, dest: &str) -> Result<Option<Strin
return Ok(None); return Ok(None);
} }
let text = response.text().await?; let text = response.limit_read_text(8192).await?;
trace!("response text: {text:?}"); trace!("response text: {text:?}");
if text.len() >= 12288 {
debug_warn!("response contains junk");
return Ok(None);
}
let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default(); let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
+20 -9
View File
@@ -72,6 +72,26 @@ where
.append_pdu(pdu, pdu_json, new_room_leaves, state_lock, room_id) .append_pdu(pdu, pdu_json, new_room_leaves, state_lock, room_id)
.await?; .await?;
// Process admin commands for federation events
if *pdu.kind() == TimelineEventType::RoomMessage {
let content: ExtractBody = pdu.get_content()?;
if let Some(body) = content.body {
if let Some(source) = self
.services
.admin
.is_admin_command(pdu, &body, false)
.await
{
self.services.admin.command_with_sender(
body,
Some(pdu.event_id().into()),
source,
pdu.sender.clone().into(),
)?;
}
}
}
Ok(Some(pdu_id)) Ok(Some(pdu_id))
} }
@@ -334,15 +354,6 @@ where
let content: ExtractBody = pdu.get_content()?; let content: ExtractBody = pdu.get_content()?;
if let Some(body) = content.body { if let Some(body) = content.body {
self.services.search.index_pdu(shortroomid, &pdu_id, &body); self.services.search.index_pdu(shortroomid, &pdu_id, &body);
if let Some(source) = self.services.admin.is_admin_command(pdu, &body).await {
self.services.admin.command_with_sender(
body,
Some((pdu.event_id()).into()),
source,
pdu.sender.clone().into(),
)?;
}
} }
}, },
| _ => {}, | _ => {},
+23 -1
View File
@@ -18,7 +18,7 @@ use ruma::{
}, },
}; };
use super::RoomMutexGuard; use super::{ExtractBody, RoomMutexGuard};
/// Creates a new persisted data unit and adds it to a room. This function /// Creates a new persisted data unit and adds it to a room. This function
/// takes a roomid_mutex_state, meaning that only this function is able to /// takes a roomid_mutex_state, meaning that only this function is able to
@@ -126,6 +126,26 @@ pub async fn build_and_append_pdu(
.boxed() .boxed()
.await?; .await?;
// Process admin commands for locally sent events
if *pdu.kind() == TimelineEventType::RoomMessage {
let content: ExtractBody = pdu.get_content()?;
if let Some(body) = content.body {
if let Some(source) = self
.services
.admin
.is_admin_command(&pdu, &body, true)
.await
{
self.services.admin.command_with_sender(
body,
Some(pdu.event_id().into()),
source,
pdu.sender.clone().into(),
)?;
}
}
}
// We set the room state after inserting the pdu, so that we never have a moment // We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist // in time where events in the current room state do not exist
trace!("Setting room state for room {room_id}"); trace!("Setting room state for room {room_id}");
@@ -167,6 +187,8 @@ pub async fn build_and_append_pdu(
Ok(pdu.event_id().to_owned()) Ok(pdu.event_id().to_owned())
} }
/// Assert invariants about the admin room, to prevent (for example) all admins
/// from leaving or being banned from the room
#[implement(super::Service)] #[implement(super::Service)]
#[tracing::instrument(skip_all, level = "debug")] #[tracing::instrument(skip_all, level = "debug")]
async fn check_pdu_for_admin_room<Pdu>(&self, pdu: &Pdu, sender: &UserId) -> Result async fn check_pdu_for_admin_room<Pdu>(&self, pdu: &Pdu, sender: &UserId) -> Result
+2 -2
View File
@@ -1,7 +1,7 @@
use std::{fmt::Debug, mem}; use std::{fmt::Debug, mem};
use bytes::BytesMut; use bytes::BytesMut;
use conduwuit::{Err, Result, debug_error, err, utils, warn}; use conduwuit::{Err, Result, debug_error, err, utils, utils::response::LimitReadExt, warn};
use reqwest::Client; use reqwest::Client;
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken}; use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
@@ -38,7 +38,7 @@ where
.expect("http::response::Builder is usable"), .expect("http::response::Builder is usable"),
); );
let body = response.bytes().await?; // TODO: handle timeout let body = response.limit_read(65535).await?; // TODO: handle timeout
if !status.is_success() { if !status.is_success() {
debug_error!("Antispam response bytes: {:?}", utils::string_from_bytes(&body)); debug_error!("Antispam response bytes: {:?}", utils::string_from_bytes(&body));
+12 -2
View File
@@ -1,7 +1,9 @@
use std::{fmt::Debug, mem}; use std::{fmt::Debug, mem};
use bytes::BytesMut; use bytes::BytesMut;
use conduwuit::{Err, Result, debug_error, err, implement, trace, utils, warn}; use conduwuit::{
Err, Result, debug_error, err, implement, trace, utils, utils::response::LimitReadExt, warn,
};
use ruma::api::{ use ruma::api::{
IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, appservice::Registration, IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, appservice::Registration,
}; };
@@ -77,7 +79,15 @@ where
.expect("http::response::Builder is usable"), .expect("http::response::Builder is usable"),
); );
let body = response.bytes().await?; let body = response
.limit_read(
self.server
.config
.max_request_size
.try_into()
.expect("usize fits into u64"),
)
.await?;
if !status.is_success() { if !status.is_success() {
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body)); debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body));
+2 -2
View File
@@ -10,7 +10,7 @@ use std::{
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use conduwuit_core::{ use conduwuit_core::{
Error, Event, Result, debug, err, error, Error, Event, Result, at, debug, err, error,
result::LogErr, result::LogErr,
trace, trace,
utils::{ utils::{
@@ -175,7 +175,7 @@ impl Service {
if !new_events.is_empty() { if !new_events.is_empty() {
self.db.mark_as_active(new_events.iter()); self.db.mark_as_active(new_events.iter());
let new_events_vec = new_events.into_iter().map(|(_, event)| event).collect(); let new_events_vec = new_events.into_iter().map(at!(1)).collect();
futures.push(self.send_events(dest.clone(), new_events_vec)); futures.push(self.send_events(dest.clone(), new_events_vec));
} else { } else {
statuses.remove(dest); statuses.remove(dest);
+149
View File
@@ -0,0 +1,149 @@
use conduwuit::{Err, Result, implement, trace};
use conduwuit_database::{Deserialized, Json};
use ruma::{
DeviceId, OwnedDeviceId, UserId,
api::client::dehydrated_device::{
DehydratedDeviceData, put_dehydrated_device::unstable::Request,
},
serde::Raw,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DehydratedDevice {
/// Unique ID of the device.
pub device_id: OwnedDeviceId,
/// Contains serialized and encrypted private data.
pub device_data: Raw<DehydratedDeviceData>,
}
/// Creates or recreates the user's dehydrated device.
#[implement(super::Service)]
#[tracing::instrument(
level = "info",
skip_all,
fields(
%user_id,
device_id = %request.device_id,
display_name = ?request.initial_device_display_name,
)
)]
pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> Result {
assert!(
self.exists(user_id).await,
"Tried to create dehydrated device for non-existent user"
);
let existing_id = self.get_dehydrated_device_id(user_id).await;
if existing_id.is_err()
&& self
.get_device_metadata(user_id, &request.device_id)
.await
.is_ok()
{
return Err!("A hydrated device already exists with that ID.");
}
if let Ok(existing_id) = existing_id {
self.remove_device(user_id, &existing_id).await;
}
self.create_device(
user_id,
&request.device_id,
"",
request.initial_device_display_name.clone(),
None,
)
.await?;
trace!(device_data = ?request.device_data);
self.db.userid_dehydrateddevice.raw_put(
user_id,
Json(&DehydratedDevice {
device_id: request.device_id.clone(),
device_data: request.device_data,
}),
);
trace!(device_keys = ?request.device_keys);
self.add_device_keys(user_id, &request.device_id, &request.device_keys)
.await;
trace!(one_time_keys = ?request.one_time_keys);
for (one_time_key_key, one_time_key_value) in &request.one_time_keys {
self.add_one_time_key(user_id, &request.device_id, one_time_key_key, one_time_key_value)
.await?;
}
Ok(())
}
/// Removes a user's dehydrated device.
///
/// Calling this directly will remove the dehydrated data but leak the frontage
/// device. Thus this is called by the regular device interface such that the
/// dehydrated data will not leak instead.
///
/// If device_id is given, the user's dehydrated device must match or this is a
/// no-op, but an Err is still returned to indicate that. Otherwise returns the
/// removed dehydrated device_id.
#[implement(super::Service)]
#[tracing::instrument(
level = "debug",
skip_all,
fields(
%user_id,
device_id = ?maybe_device_id,
)
)]
pub(super) async fn remove_dehydrated_device(
&self,
user_id: &UserId,
maybe_device_id: Option<&DeviceId>,
) -> Result<OwnedDeviceId> {
let Ok(device_id) = self.get_dehydrated_device_id(user_id).await else {
return Err!(Request(NotFound("No dehydrated device for this user.")));
};
if let Some(maybe_device_id) = maybe_device_id {
if maybe_device_id != device_id {
return Err!(Request(NotFound("Not the user's dehydrated device.")));
}
}
self.db.userid_dehydrateddevice.remove(user_id);
Ok(device_id)
}
/// Get the device_id of the user's dehydrated device.
#[implement(super::Service)]
#[tracing::instrument(
level = "debug",
skip_all,
fields(%user_id)
)]
pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result<OwnedDeviceId> {
self.get_dehydrated_device(user_id)
.await
.map(|device| device.device_id)
}
/// Get the dehydrated device private data
#[implement(super::Service)]
#[tracing::instrument(
level = "debug",
skip_all,
fields(%user_id),
ret,
)]
pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
self.db
.userid_dehydrateddevice
.get(user_id)
.await
.deserialized()
}
+12 -3
View File
@@ -1,3 +1,5 @@
pub(super) mod dehydrated_device;
#[cfg(feature = "ldap")] #[cfg(feature = "ldap")]
use std::collections::HashMap; use std::collections::HashMap;
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc}; use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
@@ -5,7 +7,7 @@ use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
#[cfg(feature = "ldap")] #[cfg(feature = "ldap")]
use conduwuit::result::LogErr; use conduwuit::result::LogErr;
use conduwuit::{ use conduwuit::{
Err, Error, Result, Server, at, debug_warn, err, is_equal_to, trace, Err, Error, Result, Server, debug_warn, err, is_equal_to, trace,
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
}; };
#[cfg(feature = "ldap")] #[cfg(feature = "ldap")]
@@ -70,6 +72,7 @@ struct Data {
userfilterid_filter: Arc<Map>, userfilterid_filter: Arc<Map>,
userid_avatarurl: Arc<Map>, userid_avatarurl: Arc<Map>,
userid_blurhash: Arc<Map>, userid_blurhash: Arc<Map>,
userid_dehydrateddevice: Arc<Map>,
userid_devicelistversion: Arc<Map>, userid_devicelistversion: Arc<Map>,
userid_displayname: Arc<Map>, userid_displayname: Arc<Map>,
userid_lastonetimekeyupdate: Arc<Map>, userid_lastonetimekeyupdate: Arc<Map>,
@@ -110,6 +113,7 @@ impl crate::Service for Service {
userfilterid_filter: args.db["userfilterid_filter"].clone(), userfilterid_filter: args.db["userfilterid_filter"].clone(),
userid_avatarurl: args.db["userid_avatarurl"].clone(), userid_avatarurl: args.db["userid_avatarurl"].clone(),
userid_blurhash: args.db["userid_blurhash"].clone(), userid_blurhash: args.db["userid_blurhash"].clone(),
userid_dehydrateddevice: args.db["userid_dehydrateddevice"].clone(),
userid_devicelistversion: args.db["userid_devicelistversion"].clone(), userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
userid_displayname: args.db["userid_displayname"].clone(), userid_displayname: args.db["userid_displayname"].clone(),
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(), userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
@@ -480,6 +484,11 @@ impl Service {
/// Removes a device from a user. /// Removes a device from a user.
pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) { pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) {
// Remove dehydrated device if this is the dehydrated device
let _: Result<_> = self
.remove_dehydrated_device(user_id, Some(device_id))
.await;
let userdeviceid = (user_id, device_id); let userdeviceid = (user_id, device_id);
// Remove tokens // Remove tokens
@@ -1003,7 +1012,7 @@ impl Service {
device_id: &'a DeviceId, device_id: &'a DeviceId,
since: Option<u64>, since: Option<u64>,
to: Option<u64>, to: Option<u64>,
) -> impl Stream<Item = Raw<AnyToDeviceEvent>> + Send + 'a { ) -> impl Stream<Item = (u64, Raw<AnyToDeviceEvent>)> + Send + 'a {
type Key<'a> = (&'a UserId, &'a DeviceId, u64); type Key<'a> = (&'a UserId, &'a DeviceId, u64);
let from = (user_id, device_id, since.map_or(0, |since| since.saturating_add(1))); let from = (user_id, device_id, since.map_or(0, |since| since.saturating_add(1)));
@@ -1017,7 +1026,7 @@ impl Service {
&& device_id == *device_id_ && device_id == *device_id_
&& to.is_none_or(|to| *count <= to) && to.is_none_or(|to| *count <= to)
}) })
.map(at!(1)) .map(|((_, _, count), event)| (count, event))
} }
pub async fn remove_to_device_events<Until>( pub async fn remove_to_device_events<Until>(
+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"}