diff --git a/crates/ruma-client-api/src/sync/sync_events/v4.rs b/crates/ruma-client-api/src/sync/sync_events/v4.rs index edc4ccf8..604d5ccc 100644 --- a/crates/ruma-client-api/src/sync/sync_events/v4.rs +++ b/crates/ruma-client-api/src/sync/sync_events/v4.rs @@ -23,7 +23,9 @@ use ruma_events::{ }; use serde::{de::Error as _, Deserialize, Serialize}; -use super::{v5, DeviceLists, UnreadNotificationsCount}; +#[cfg(feature = "unstable-msc4186")] +use super::v5; +use super::{DeviceLists, UnreadNotificationsCount}; const METADATA: Metadata = metadata! { method: POST, @@ -928,6 +930,7 @@ impl Typing { } } +#[cfg(feature = "unstable-msc4186")] impl From for Request { fn from(value: v5::Request) -> Self { Self { @@ -952,6 +955,7 @@ impl From for Request { } } +#[cfg(feature = "unstable-msc4186")] impl From for SyncRequestList { fn from(value: v5::request::List) -> Self { Self { @@ -974,12 +978,14 @@ impl From for SyncRequestList { } } +#[cfg(feature = "unstable-msc4186")] impl From for RoomDetailsConfig { fn from(value: v5::request::RoomDetails) -> Self { Self { required_state: value.required_state, timeline_limit: value.timeline_limit } } } +#[cfg(feature = "unstable-msc4186")] impl From for SyncRequestListFilters { fn from(value: v5::request::ListFilters) -> Self { Self { @@ -990,6 +996,7 @@ impl From for SyncRequestListFilters { } } +#[cfg(feature = "unstable-msc4186")] impl From for RoomSubscription { fn from(value: v5::request::RoomSubscription) -> Self { Self { @@ -1000,6 +1007,7 @@ impl From for RoomSubscription { } } +#[cfg(feature = "unstable-msc4186")] impl From for ExtensionsConfig { fn from(value: v5::request::Extensions) -> Self { Self { @@ -1014,6 +1022,7 @@ impl From for ExtensionsConfig { } } +#[cfg(feature = "unstable-msc4186")] impl From for ToDeviceConfig { fn from(value: v5::request::ToDevice) -> Self { Self { @@ -1026,18 +1035,21 @@ impl From for ToDeviceConfig { } } +#[cfg(feature = "unstable-msc4186")] impl From for E2EEConfig { fn from(value: v5::request::E2EE) -> Self { Self { enabled: value.enabled } } } +#[cfg(feature = "unstable-msc4186")] impl From for AccountDataConfig { fn from(value: v5::request::AccountData) -> Self { Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms } } } +#[cfg(feature = "unstable-msc4186")] impl From for ReceiptsConfig { fn from(value: v5::request::Receipts) -> Self { Self { @@ -1048,6 +1060,7 @@ impl From for ReceiptsConfig { } } +#[cfg(feature = "unstable-msc4186")] impl From for RoomReceiptConfig { fn from(value: v5::request::ReceiptsRoom) -> Self { match value { @@ -1057,6 +1070,7 @@ impl From for RoomReceiptConfig { } } +#[cfg(feature = "unstable-msc4186")] impl From for TypingConfig { fn from(value: v5::request::Typing) -> Self { Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms } diff --git a/crates/ruma-events/CHANGELOG.md b/crates/ruma-events/CHANGELOG.md index f024b5b3..683bbc6d 100644 --- a/crates/ruma-events/CHANGELOG.md +++ b/crates/ruma-events/CHANGELOG.md @@ -18,6 +18,11 @@ Improvements: - Stabilize support for muting in VoIP calls, according to Matrix 1.11 - All the root `Any*EventContent` types now have a `EventContentFromType` implementations automatically derived by the `event_enum!` macro. +- `CallMemberEventContent` now supports two different formats: Session memberships and Legacy memberships. +The new format (Session) is required to reliably display the call member count (reliable call member events). +`CallMemberEventContent` is now an enum to model the two different formats. +- `CallMemberStateKey` (instead of `OwnedUserId`) is now used as the state key type for `CallMemberEventContent`. +This guarantees correct formatting of the event key. Breaking changes: diff --git a/crates/ruma-events/src/call/member.rs b/crates/ruma-events/src/call/member.rs index c193249e..a31b795a 100644 --- a/crates/ruma-events/src/call/member.rs +++ b/crates/ruma-events/src/call/member.rs @@ -11,7 +11,7 @@ mod member_state_key; pub use focus::*; pub use member_data::*; pub use member_state_key::*; -use ruma_common::MilliSecondsSinceUnixEpoch; +use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedDeviceId}; use ruma_macros::{EventContent, StringEnum}; use serde::{Deserialize, Serialize}; @@ -56,7 +56,7 @@ impl CallMemberEventContent { /// Creates a new [`CallMemberEventContent`] with [`SessionMembershipData`]. pub fn new( application: Application, - device_id: String, + device_id: OwnedDeviceId, focus_active: ActiveFocus, foci_preferred: Vec, created_ts: Option, @@ -234,8 +234,8 @@ mod tests { use assert_matches2::assert_matches; use ruma_common::{ - device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, - OwnedUserId, + device_id, owned_device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId, + OwnedRoomId, OwnedUserId, }; use serde_json::{from_value as from_json_value, json, Value as JsonValue}; @@ -257,7 +257,7 @@ mod tests { call_id: "123456".to_owned(), scope: CallScope::Room, }), - device_id: "ABCDE".to_owned(), + device_id: owned_device_id!("ABCDE"), expires: Duration::from_secs(3600), foci_active: vec![Focus::Livekit(LivekitFocus { alias: "1".to_owned(), @@ -274,7 +274,7 @@ mod tests { call_id: "123456".to_owned(), scope: CallScope::Room, }), - "ABCDE".to_owned(), + owned_device_id!("ABCDE"), ActiveFocus::Livekit(ActiveLivekitFocus { focus_selection: FocusSelection::OldestMembership, }), @@ -354,7 +354,7 @@ mod tests { call_id: "123456".to_owned(), scope: CallScope::Room, }), - "THIS_DEVICE".to_owned(), + owned_device_id!("THIS_DEVICE"), ActiveFocus::Livekit(ActiveLivekitFocus { focus_selection: FocusSelection::OldestMembership, }), @@ -404,7 +404,7 @@ mod tests { call_id: "123456".to_owned(), scope: CallScope::Room, }), - device_id: "THIS_DEVICE".to_owned(), + device_id: owned_device_id!("THIS_DEVICE"), expires: Duration::from_secs(3600), foci_active: vec![Focus::Livekit(LivekitFocus { alias: "room1".to_owned(), @@ -418,7 +418,7 @@ mod tests { call_id: "".to_owned(), scope: CallScope::Room, }), - device_id: "OTHER_DEVICE".to_owned(), + device_id: owned_device_id!("OTHER_DEVICE"), expires: Duration::from_secs(3600), foci_active: vec![Focus::Livekit(LivekitFocus { alias: "room2".to_owned(), @@ -526,7 +526,7 @@ mod tests { call_id: "".to_owned(), scope: CallScope::Room, }), - device_id: "THIS_DEVICE".to_owned(), + device_id: owned_device_id!("THIS_DEVICE"), foci_preferred: [Focus::Livekit(LivekitFocus { alias: "room1".to_owned(), service_url: "https://livekit1.com".to_owned(), diff --git a/crates/ruma-events/src/call/member/member_data.rs b/crates/ruma-events/src/call/member/member_data.rs index 2939372d..6804c570 100644 --- a/crates/ruma-events/src/call/member/member_data.rs +++ b/crates/ruma-events/src/call/member/member_data.rs @@ -5,7 +5,7 @@ use std::time::Duration; use as_variant::as_variant; -use ruma_common::MilliSecondsSinceUnixEpoch; +use ruma_common::{DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId}; use ruma_macros::StringEnum; use serde::{Deserialize, Serialize}; use tracing::warn; @@ -41,7 +41,7 @@ impl<'a> MembershipData<'a> { } /// The device id of this membership. - pub fn device_id(&self) -> &String { + pub fn device_id(&self) -> &DeviceId { match self { MembershipData::Legacy(data) => &data.device_id, MembershipData::Session(data) => &data.device_id, @@ -121,7 +121,7 @@ pub struct LegacyMembershipData { /// The device id of this membership. /// /// The same user can join with their phone/computer. - pub device_id: String, + pub device_id: OwnedDeviceId, /// The duration in milliseconds relative to the time this membership joined /// during which the membership is valid. @@ -190,7 +190,7 @@ pub struct LegacyMembershipDataInit { /// The device id of this membership. /// /// The same user can join with their phone/computer. - pub device_id: String, + pub device_id: OwnedDeviceId, /// The duration in milliseconds relative to the time this membership joined /// during which the membership is valid. @@ -236,7 +236,7 @@ pub struct SessionMembershipData { /// The device id of this membership. /// /// The same user can join with their phone/computer. - pub device_id: String, + pub device_id: OwnedDeviceId, /// A list of the foci that this membership proposes to use. pub foci_preferred: Vec, diff --git a/crates/ruma-events/src/call/member/member_state_key.rs b/crates/ruma-events/src/call/member/member_state_key.rs index b593d499..f50cb773 100644 --- a/crates/ruma-events/src/call/member/member_state_key.rs +++ b/crates/ruma-events/src/call/member/member_state_key.rs @@ -16,11 +16,11 @@ pub struct CallMemberStateKey { impl CallMemberStateKey { /// Constructs a new CallMemberStateKey there are three possible formats: - /// - "_{UserId}_{DeviceId}" example: "_@test:user.org_DEVICE". `device_id`: Some`, `underscore: + /// - `_{UserId}_{DeviceId}` example: `_@test:user.org_DEVICE`. `device_id: Some`, `underscore: /// true` - /// - "{UserId}_{DeviceId}" example: "@test:user.org_DEVICE". `device_id`: Some`, `underscore: + /// - `{UserId}_{DeviceId}` example: `@test:user.org_DEVICE`. `device_id: Some`, `underscore: /// false` - /// - "{UserId}" example example: "@test:user.org". `device_id`: None`, underscore is ignored: + /// - `{UserId}` example: `@test:user.org`. `device_id: None`, underscore is ignored: /// `underscore: false|true` /// /// Dependent on the parameters the correct CallMemberStateKey will be constructed. diff --git a/crates/ruma-federation-api/src/authenticated_media.rs b/crates/ruma-federation-api/src/authenticated_media.rs index 03bb753e..f19e9f57 100644 --- a/crates/ruma-federation-api/src/authenticated_media.rs +++ b/crates/ruma-federation-api/src/authenticated_media.rs @@ -174,12 +174,20 @@ fn try_from_multipart_mixed_response>( let mut full_boundary = Vec::with_capacity(boundary.len() + 4); full_boundary.extend_from_slice(b"\r\n--"); full_boundary.extend_from_slice(boundary); + let full_boundary_no_crlf = full_boundary.strip_prefix(b"\r\n").unwrap(); let mut boundaries = memchr::memmem::find_iter(body, &full_boundary); - let metadata_start = boundaries.next().ok_or_else(|| { - MultipartMixedDeserializationError::MissingBodyParts { expected: 2, found: 0 } - })? + full_boundary.len(); + let metadata_start = if body.starts_with(full_boundary_no_crlf) { + // If there is no preamble before the first boundary, it may omit the + // preceding CRLF. + full_boundary_no_crlf.len() + } else { + boundaries.next().ok_or_else(|| MultipartMixedDeserializationError::MissingBodyParts { + expected: 2, + found: 0, + })? + full_boundary.len() + }; let metadata_end = boundaries.next().ok_or_else(|| { MultipartMixedDeserializationError::MissingBodyParts { expected: 2, found: 0 } })?; @@ -417,6 +425,15 @@ mod tests { .unwrap(); try_from_multipart_mixed_response(response).unwrap_err(); + + // Boundary without CRLF with preamble. + let body = "foo--abcdef\r\n\r\n{}\r\n--abcdef\r\n\r\nsome plain text\r\n--abcdef--"; + let response = http::Response::builder() + .header(http::header::CONTENT_TYPE, "multipart/mixed; boundary=abcdef") + .body(body) + .unwrap(); + + try_from_multipart_mixed_response(response).unwrap_err(); } #[test] @@ -483,6 +500,36 @@ mod tests { assert_eq!(file_content.content_type.unwrap(), "text/plain"); assert_eq!(file_content.content_disposition, None); + // No leading CRLF (and no preamble) + let body = "--abcdef\r\n\r\n{}\r\n--abcdef\r\n\r\nsome plain text\r\n--abcdef--"; + let response = http::Response::builder() + .header(http::header::CONTENT_TYPE, "multipart/mixed; boundary=abcdef") + .body(body) + .unwrap(); + + let (_metadata, content) = try_from_multipart_mixed_response(response).unwrap(); + + assert_matches!(content, FileOrLocation::File(file_content)); + assert_eq!(file_content.file, b"some plain text"); + assert_eq!(file_content.content_type, None); + assert_eq!(file_content.content_disposition, None); + + // Boundary text in preamble, but no leading CRLF, so it should be + // ignored. + let body = + "foo--abcdef\r\n--abcdef\r\n\r\n{}\r\n--abcdef\r\n\r\nsome plain text\r\n--abcdef--"; + let response = http::Response::builder() + .header(http::header::CONTENT_TYPE, "multipart/mixed; boundary=abcdef") + .body(body) + .unwrap(); + + let (_metadata, content) = try_from_multipart_mixed_response(response).unwrap(); + + assert_matches!(content, FileOrLocation::File(file_content)); + assert_eq!(file_content.file, b"some plain text"); + assert_eq!(file_content.content_type, None); + assert_eq!(file_content.content_disposition, None); + // No body part headers. let body = "\r\n--abcdef\r\n\r\n{}\r\n--abcdef\r\n\r\nsome plain text\r\n--abcdef--"; let response = http::Response::builder()