Merge remote-tracking branch 'upstream/main' into conduwuit-changes
This commit is contained in:
		
						commit
						9a5bfad849
					
				| @ -137,6 +137,13 @@ use super::MyType; | |||||||
| 
 | 
 | ||||||
| ### Commit Messages | ### Commit Messages | ||||||
| 
 | 
 | ||||||
|  | The commit message should start with the _area_ that is affected by the change. | ||||||
|  | An area is usually the name of the affected crate without the `ruma-` prefix, | ||||||
|  | except for the ruma-common crate, where the area is usually the name of the | ||||||
|  | top-level module, like `api` or `identifiers`. For example, the description of | ||||||
|  | a commit that affects the ruma-events crate should look like | ||||||
|  | "events: Add new event". | ||||||
|  | 
 | ||||||
| Write commit messages using the imperative mood, as if completing the sentence: | Write commit messages using the imperative mood, as if completing the sentence: | ||||||
| "If applied, this commit will \_\_\_." For example, use "Fix some bug" instead | "If applied, this commit will \_\_\_." For example, use "Fix some bug" instead | ||||||
| of "Fixed some bug" or "Add a feature" instead of "Added a feature". | of "Fixed some bug" or "Add a feature" instead of "Added a feature". | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ pub mod v1 { | |||||||
| 
 | 
 | ||||||
|         /// One or more custom fields to help identify the third party location.
 |         /// One or more custom fields to help identify the third party location.
 | ||||||
|         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 |         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 | ||||||
|         #[ruma_api(query_map)] |         #[ruma_api(query_all)] | ||||||
|         pub fields: BTreeMap<String, String>, |         pub fields: BTreeMap<String, String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ pub mod v1 { | |||||||
| 
 | 
 | ||||||
|         /// One or more custom fields that are passed to the AS to help identify the user.
 |         /// One or more custom fields that are passed to the AS to help identify the user.
 | ||||||
|         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 |         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 | ||||||
|         #[ruma_api(query_map)] |         #[ruma_api(query_all)] | ||||||
|         pub fields: BTreeMap<String, String>, |         pub fields: BTreeMap<String, String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ Breaking changes: | |||||||
|   as before. |   as before. | ||||||
| - Change type of `client_secret` field in `ThirdpartyIdCredentials` | - Change type of `client_secret` field in `ThirdpartyIdCredentials` | ||||||
|   from `Box<ClientSecret>` to `OwnedClientSecret` |   from `Box<ClientSecret>` to `OwnedClientSecret` | ||||||
|  | - Make `id_server` and `id_access_token` in `ThirdpartyIdCredentials` optional | ||||||
| 
 | 
 | ||||||
| Improvements: | Improvements: | ||||||
| 
 | 
 | ||||||
| @ -13,15 +14,23 @@ Improvements: | |||||||
| - Heroes in `sync::sync_events::v4`: `SyncRequestList` and `RoomSubscription` | - Heroes in `sync::sync_events::v4`: `SyncRequestList` and `RoomSubscription` | ||||||
|   both have a new `include_heroes` field. `SlidingSyncRoom` has a new `heroes` |   both have a new `include_heroes` field. `SlidingSyncRoom` has a new `heroes` | ||||||
|   field, with a new type `SlidingSyncRoomHero`. |   field, with a new type `SlidingSyncRoomHero`. | ||||||
| - Add unstable support for authenticated media endpoints, according to MSC3916. | - Add support for authenticated media endpoints, according to MSC3916 / Matrix | ||||||
| 
 |   1.11. | ||||||
| Bug fixes: |   - They replace the newly deprecated `media::get_*` endpoints. | ||||||
| 
 | - Stabilize support for animated thumbnails, according to Matrix 1.11 | ||||||
| - Rename `avatar` to `avatar_url` when (De)serializing | - Add support for terms of service at registration, according to MSC1692 / | ||||||
|  |   Matrix 1.11 | ||||||
|  | - Add unstable support for [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) | ||||||
|  |   to send `Future` events and update `Future` events with `future_tokens`. | ||||||
|  |   (`Future` events are scheduled messages that can be controlled | ||||||
|  |   with `future_tokens` to send on demand or restart the timeout) | ||||||
| 
 | 
 | ||||||
| Bug fixes: | Bug fixes: | ||||||
| 
 | 
 | ||||||
|  | - Rename `avatar` to `avatar_url` when (De)serializing `SlidingSyncRoomHero` | ||||||
| - `user_id` of `SlidingSyncRoomHero` is now mandatory | - `user_id` of `SlidingSyncRoomHero` is now mandatory | ||||||
|  | - Make authentication with access token optional for the `change_password` and | ||||||
|  |   `deactivate` endpoints. | ||||||
| 
 | 
 | ||||||
| # 0.18.0 | # 0.18.0 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -40,7 +40,6 @@ unstable-exhaustive-types = ["ruma-common/unstable-exhaustive-types"] | |||||||
| unstable-msc2666 = [] | unstable-msc2666 = [] | ||||||
| unstable-msc2448 = [] | unstable-msc2448 = [] | ||||||
| unstable-msc2654 = [] | unstable-msc2654 = [] | ||||||
| unstable-msc2705 = [] |  | ||||||
| unstable-msc2965 = [] | unstable-msc2965 = [] | ||||||
| unstable-msc2967 = [] | unstable-msc2967 = [] | ||||||
| unstable-msc3266 = [] | unstable-msc3266 = [] | ||||||
| @ -48,10 +47,10 @@ unstable-msc3488 = [] | |||||||
| unstable-msc3575 = [] | unstable-msc3575 = [] | ||||||
| unstable-msc3814 = [] | unstable-msc3814 = [] | ||||||
| unstable-msc3843 = [] | unstable-msc3843 = [] | ||||||
| unstable-msc3916 = [] |  | ||||||
| unstable-msc3983 = [] | unstable-msc3983 = [] | ||||||
| unstable-msc4108 = [] | unstable-msc4108 = [] | ||||||
| unstable-msc4121 = [] | unstable-msc4121 = [] | ||||||
|  | unstable-msc4140 = [] | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| as_variant = { workspace = true } | as_variant = { workspace = true } | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ pub mod v3 { | |||||||
|     const METADATA: Metadata = metadata! { |     const METADATA: Metadata = metadata! { | ||||||
|         method: POST, |         method: POST, | ||||||
|         rate_limited: true, |         rate_limited: true, | ||||||
|         authentication: AccessToken, |         authentication: AccessTokenOptional, | ||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/client/r0/account/password", |             1.0 => "/_matrix/client/r0/account/password", | ||||||
|             1.1 => "/_matrix/client/v3/account/password", |             1.1 => "/_matrix/client/v3/account/password", | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ pub mod v3 { | |||||||
|     const METADATA: Metadata = metadata! { |     const METADATA: Metadata = metadata! { | ||||||
|         method: POST, |         method: POST, | ||||||
|         rate_limited: true, |         rate_limited: true, | ||||||
|         authentication: AccessToken, |         authentication: AccessTokenOptional, | ||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/client/r0/account/deactivate", |             1.0 => "/_matrix/client/r0/account/deactivate", | ||||||
|             1.1 => "/_matrix/client/v3/account/deactivate", |             1.1 => "/_matrix/client/v3/account/deactivate", | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| //! Authenticated endpoints for the media repository, according to [MSC3916].
 | //! Authenticated endpoints for the [content repository].
 | ||||||
| //!
 | //!
 | ||||||
| //! [MSC3916]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
 | //! [content repository]: https://spec.matrix.org/latest/client-server-api/#content-repository
 | ||||||
| 
 | 
 | ||||||
| pub mod get_content; | pub mod get_content; | ||||||
| pub mod get_content_as_filename; | pub mod get_content_as_filename; | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| //!
 | //!
 | ||||||
| //! Retrieve content from the media store.
 | //! Retrieve content from the media store.
 | ||||||
| 
 | 
 | ||||||
| pub mod unstable { | pub mod v1 { | ||||||
|     //! `/unstable/org.matrix.msc3916/` ([MSC])
 |     //! `/v1/` ([spec])
 | ||||||
|     //!
 |     //!
 | ||||||
|     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
 |     //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediadownloadservernamemediaid
 | ||||||
| 
 | 
 | ||||||
|     use std::time::Duration; |     use std::time::Duration; | ||||||
| 
 | 
 | ||||||
| @ -17,10 +17,11 @@ pub mod unstable { | |||||||
| 
 | 
 | ||||||
|     const METADATA: Metadata = metadata! { |     const METADATA: Metadata = metadata! { | ||||||
|         method: GET, |         method: GET, | ||||||
|         rate_limited: false, |         rate_limited: true, | ||||||
|         authentication: AccessToken, |         authentication: AccessToken, | ||||||
|         history: { |         history: { | ||||||
|             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id", |             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id", | ||||||
|  |             1.11 => "/_matrix/client/v1/media/download/:server_name/:media_id", | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| //!
 | //!
 | ||||||
| //! Retrieve content from the media store, specifying a filename to return.
 | //! Retrieve content from the media store, specifying a filename to return.
 | ||||||
| 
 | 
 | ||||||
| pub mod unstable { | pub mod v1 { | ||||||
|     //! `/unstable/org.matrix.msc3916/` ([MSC])
 |     //! `/v1/` ([spec])
 | ||||||
|     //!
 |     //!
 | ||||||
|     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
 |     //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediadownloadservernamemediaidfilename
 | ||||||
| 
 | 
 | ||||||
|     use std::time::Duration; |     use std::time::Duration; | ||||||
| 
 | 
 | ||||||
| @ -17,10 +17,11 @@ pub mod unstable { | |||||||
| 
 | 
 | ||||||
|     const METADATA: Metadata = metadata! { |     const METADATA: Metadata = metadata! { | ||||||
|         method: GET, |         method: GET, | ||||||
|         rate_limited: false, |         rate_limited: true, | ||||||
|         authentication: AccessToken, |         authentication: AccessToken, | ||||||
|         history: { |         history: { | ||||||
|             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id/:filename", |             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id/:filename", | ||||||
|  |             1.11 => "/_matrix/client/v1/media/download/:server_name/:media_id/:filename", | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| //!
 | //!
 | ||||||
| //! Get a thumbnail of content from the media store.
 | //! Get a thumbnail of content from the media store.
 | ||||||
| 
 | 
 | ||||||
| pub mod unstable { | pub mod v1 { | ||||||
|     //! `/unstable/org.matrix.msc3916/` ([MSC])
 |     //! `/v1/` ([spec])
 | ||||||
|     //!
 |     //!
 | ||||||
|     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
 |     //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediathumbnailservernamemediaid
 | ||||||
| 
 | 
 | ||||||
|     use std::time::Duration; |     use std::time::Duration; | ||||||
| 
 | 
 | ||||||
| @ -24,6 +24,7 @@ pub mod unstable { | |||||||
|         authentication: AccessToken, |         authentication: AccessToken, | ||||||
|         history: { |         history: { | ||||||
|             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/:server_name/:media_id", |             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/:server_name/:media_id", | ||||||
|  |             1.11 => "/_matrix/client/v1/media/thumbnail/:server_name/:media_id", | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| @ -69,18 +70,12 @@ pub mod unstable { | |||||||
| 
 | 
 | ||||||
|         /// Whether the server should return an animated thumbnail.
 |         /// Whether the server should return an animated thumbnail.
 | ||||||
|         ///
 |         ///
 | ||||||
|         /// When `true`, the server should return an animated thumbnail if possible and supported.
 |         /// When `Some(true)`, the server should return an animated thumbnail if possible and
 | ||||||
|         /// Otherwise it must not return an animated thumbnail.
 |         /// supported. When `Some(false)`, the server must not return an animated
 | ||||||
|         ///
 |         /// thumbnail. When `None`, the server should not return an animated thumbnail.
 | ||||||
|         /// Defaults to `false`.
 |  | ||||||
|         #[cfg(feature = "unstable-msc2705")] |  | ||||||
|         #[ruma_api(query)] |         #[ruma_api(query)] | ||||||
|         #[serde(
 |         #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|             rename = "org.matrix.msc2705.animated", |         pub animated: Option<bool>, | ||||||
|             default, |  | ||||||
|             skip_serializing_if = "ruma_common::serde::is_default" |  | ||||||
|         )] |  | ||||||
|         pub animated: bool, |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Response type for the `get_content_thumbnail` endpoint.
 |     /// Response type for the `get_content_thumbnail` endpoint.
 | ||||||
| @ -111,8 +106,7 @@ pub mod unstable { | |||||||
|                 width, |                 width, | ||||||
|                 height, |                 height, | ||||||
|                 timeout_ms: crate::media::default_download_timeout(), |                 timeout_ms: crate::media::default_download_timeout(), | ||||||
|                 #[cfg(feature = "unstable-msc2705")] |                 animated: None, | ||||||
|                 animated: false, |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| //!
 | //!
 | ||||||
| //! Gets the config for the media repository.
 | //! Gets the config for the media repository.
 | ||||||
| 
 | 
 | ||||||
| pub mod unstable { | pub mod v1 { | ||||||
|     //! `/unstable/org.matrix.msc3916/` ([MSC])
 |     //! `/v1/` ([spec])
 | ||||||
|     //!
 |     //!
 | ||||||
|     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
 |     //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediaconfig
 | ||||||
| 
 | 
 | ||||||
|     use js_int::UInt; |     use js_int::UInt; | ||||||
|     use ruma_common::{ |     use ruma_common::{ | ||||||
| @ -19,6 +19,7 @@ pub mod unstable { | |||||||
|         authentication: AccessToken, |         authentication: AccessToken, | ||||||
|         history: { |         history: { | ||||||
|             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/config", |             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/config", | ||||||
|  |             1.11 => "/_matrix/client/v1/media/config", | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| //!
 | //!
 | ||||||
| //! Get a preview for a URL.
 | //! Get a preview for a URL.
 | ||||||
| 
 | 
 | ||||||
| pub mod unstable { | pub mod v1 { | ||||||
|     //! `/unstable/org.matrix.msc3916/` ([MSC])
 |     //! `/v1/` ([spec])
 | ||||||
|     //!
 |     //!
 | ||||||
|     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
 |     //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediapreview_url
 | ||||||
| 
 | 
 | ||||||
|     use ruma_common::{ |     use ruma_common::{ | ||||||
|         api::{request, response, Metadata}, |         api::{request, response, Metadata}, | ||||||
| @ -20,6 +20,7 @@ pub mod unstable { | |||||||
|         authentication: AccessToken, |         authentication: AccessToken, | ||||||
|         history: { |         history: { | ||||||
|             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url", |             unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url", | ||||||
|  |             1.11 => "/_matrix/client/v1/media/preview_url", | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										38
									
								
								crates/ruma-client-api/src/future.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								crates/ruma-client-api/src/future.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | //! Endpoints for sending and receiving futures
 | ||||||
|  | 
 | ||||||
|  | pub mod send_future_message_event; | ||||||
|  | pub mod send_future_state_event; | ||||||
|  | pub mod update_future; | ||||||
|  | 
 | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use web_time::Duration; | ||||||
|  | 
 | ||||||
|  | /// The query parameters for a future request.
 | ||||||
|  | /// It can contain the possible timeout and the future_group_id combinations.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[serde(untagged)] | ||||||
|  | pub enum FutureParameters { | ||||||
|  |     /// Only sending the timeout creates a timeout future with a new (server generated)
 | ||||||
|  |     /// group id. The optional group id is used to create a secondary timeout.  
 | ||||||
|  |     /// In a future group with two timeouts only one of them will ever be sent.
 | ||||||
|  |     Timeout { | ||||||
|  |         /// The timeout duration for this Future.
 | ||||||
|  |         #[serde(with = "ruma_common::serde::duration::ms")] | ||||||
|  |         #[serde(rename = "future_timeout")] | ||||||
|  |         timeout: Duration, | ||||||
|  |         /// The associated group for this Future.
 | ||||||
|  |         #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |         #[serde(rename = "future_group_id")] | ||||||
|  |         group_id: Option<String>, | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /// Adds an additional action to a future without a timeout but requires a future group_id.
 | ||||||
|  |     /// A possible matrix event that this future group can resolve to. It can be sent by using the
 | ||||||
|  |     /// send_token as an alternative to the timeout future of an already existing group.
 | ||||||
|  |     Action { | ||||||
|  |         /// The associated group for this Future.
 | ||||||
|  |         #[serde(rename = "future_group_id")] | ||||||
|  |         group_id: String, | ||||||
|  |     }, | ||||||
|  | } | ||||||
							
								
								
									
										182
									
								
								crates/ruma-client-api/src/future/send_future_message_event.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								crates/ruma-client-api/src/future/send_future_message_event.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,182 @@ | |||||||
|  | //! `PUT /_matrix/client/*/rooms/{roomId}/send_future/{eventType}/{txnId}`
 | ||||||
|  | //!
 | ||||||
|  | //! Send a future (a scheduled message) to a room. [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)
 | ||||||
|  | 
 | ||||||
|  | pub mod unstable { | ||||||
|  |     //! `msc4140` ([MSC])
 | ||||||
|  |     //!
 | ||||||
|  |     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
 | ||||||
|  | 
 | ||||||
|  |     use ruma_common::{ | ||||||
|  |         api::{request, response, Metadata}, | ||||||
|  |         metadata, | ||||||
|  |         serde::Raw, | ||||||
|  |         OwnedRoomId, OwnedTransactionId, | ||||||
|  |     }; | ||||||
|  |     use ruma_events::{AnyMessageLikeEventContent, MessageLikeEventContent, MessageLikeEventType}; | ||||||
|  |     use serde_json::value::to_raw_value as to_raw_json_value; | ||||||
|  | 
 | ||||||
|  |     use crate::future::FutureParameters; | ||||||
|  | 
 | ||||||
|  |     const METADATA: Metadata = metadata! { | ||||||
|  |         method: PUT, | ||||||
|  |         rate_limited: false, | ||||||
|  |         authentication: AccessToken, | ||||||
|  |         history: { | ||||||
|  |             unstable => "/_matrix/client/unstable/org.matrix.msc4140/rooms/:room_id/send_future/:event_type/:txn_id", | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     /// Request type for the [`send_future_message_event`](crate::future::send_future_message_event)
 | ||||||
|  |     /// endpoint.
 | ||||||
|  |     #[request(error = crate::Error)] | ||||||
|  |     pub struct Request { | ||||||
|  |         /// The room to send the event to.
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub room_id: OwnedRoomId, | ||||||
|  | 
 | ||||||
|  |         /// The type of event to send.
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub event_type: MessageLikeEventType, | ||||||
|  | 
 | ||||||
|  |         /// The transaction ID for this event.
 | ||||||
|  |         ///
 | ||||||
|  |         /// Clients should generate a unique ID across requests within the
 | ||||||
|  |         /// same session. A session is identified by an access token, and
 | ||||||
|  |         /// persists when the [access token is refreshed].
 | ||||||
|  |         ///
 | ||||||
|  |         /// It will be used by the server to ensure idempotency of requests.
 | ||||||
|  |         ///
 | ||||||
|  |         /// [access token is refreshed]: https://spec.matrix.org/latest/client-server-api/#refreshing-access-tokens
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub txn_id: OwnedTransactionId, | ||||||
|  | 
 | ||||||
|  |         /// Additional parameters to describe sending a future.
 | ||||||
|  |         ///
 | ||||||
|  |         /// Only three combinations for `future_timeout` and `future_group_id` are possible.
 | ||||||
|  |         /// The enum [`FutureParameters`] enforces this.
 | ||||||
|  |         #[ruma_api(query_all)] | ||||||
|  |         pub future_parameters: FutureParameters, | ||||||
|  | 
 | ||||||
|  |         /// The event content to send.
 | ||||||
|  |         #[ruma_api(body)] | ||||||
|  |         pub body: Raw<AnyMessageLikeEventContent>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Response type for the
 | ||||||
|  |     /// [`send_future_message_event`](crate::future::send_future_message_event) endpoint.
 | ||||||
|  |     #[response(error = crate::Error)] | ||||||
|  |     pub struct Response { | ||||||
|  |         /// A token to send/insert the future into the DAG.
 | ||||||
|  |         pub send_token: String, | ||||||
|  |         /// A token to cancel this future. It will never be send if this is called.
 | ||||||
|  |         pub cancel_token: String, | ||||||
|  |         /// The `future_group_id` generated for this future. Used to connect multiple futures
 | ||||||
|  |         /// only one of the connected futures will be sent and inserted into the DAG.
 | ||||||
|  |         pub future_group_id: String, | ||||||
|  |         /// A token used to refresh the timer of the future. This allows
 | ||||||
|  |         /// to implement heartbeat like capabilities. An event is only sent once
 | ||||||
|  |         /// a refresh in the timeout interval is missed.
 | ||||||
|  |         ///
 | ||||||
|  |         /// If the future does not have a timeout this will be `None`.
 | ||||||
|  |         pub refresh_token: Option<String>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl Request { | ||||||
|  |         /// Creates a new `Request` with the given room id, transaction id future_parameters and
 | ||||||
|  |         /// event content.
 | ||||||
|  |         ///
 | ||||||
|  |         /// # Errors
 | ||||||
|  |         ///
 | ||||||
|  |         /// Since `Request` stores the request body in serialized form, this function can fail if
 | ||||||
|  |         /// `T`s [`::serde::Serialize`] implementation can fail.
 | ||||||
|  |         pub fn new<T>( | ||||||
|  |             room_id: OwnedRoomId, | ||||||
|  |             txn_id: OwnedTransactionId, | ||||||
|  |             future_parameters: FutureParameters, | ||||||
|  |             content: &T, | ||||||
|  |         ) -> serde_json::Result<Self> | ||||||
|  |         where | ||||||
|  |             T: MessageLikeEventContent, | ||||||
|  |         { | ||||||
|  |             Ok(Self { | ||||||
|  |                 room_id, | ||||||
|  |                 txn_id, | ||||||
|  |                 event_type: content.event_type(), | ||||||
|  |                 future_parameters, | ||||||
|  |                 body: Raw::from_json(to_raw_json_value(content)?), | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// Creates a new `Request` with the given room id, transaction id, event type,
 | ||||||
|  |         /// future parameters and raw event content.
 | ||||||
|  |         pub fn new_raw( | ||||||
|  |             room_id: OwnedRoomId, | ||||||
|  |             txn_id: OwnedTransactionId, | ||||||
|  |             event_type: MessageLikeEventType, | ||||||
|  |             future_parameters: FutureParameters, | ||||||
|  |             body: Raw<AnyMessageLikeEventContent>, | ||||||
|  |         ) -> Self { | ||||||
|  |             Self { room_id, event_type, txn_id, future_parameters, body } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl Response { | ||||||
|  |         /// Creates a new `Response` with the tokens required to control the future using the
 | ||||||
|  |         /// [`crate::future::update_future::unstable::Request`] request.
 | ||||||
|  |         pub fn new( | ||||||
|  |             send_token: String, | ||||||
|  |             cancel_token: String, | ||||||
|  |             future_group_id: String, | ||||||
|  |             refresh_token: Option<String>, | ||||||
|  |         ) -> Self { | ||||||
|  |             Self { send_token, cancel_token, future_group_id, refresh_token } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(all(test, feature = "client"))] | ||||||
|  |     mod tests { | ||||||
|  |         use ruma_common::{ | ||||||
|  |             api::{MatrixVersion, OutgoingRequest, SendAccessToken}, | ||||||
|  |             owned_room_id, | ||||||
|  |         }; | ||||||
|  |         use ruma_events::room::message::RoomMessageEventContent; | ||||||
|  |         use serde_json::{json, Value as JsonValue}; | ||||||
|  |         use web_time::Duration; | ||||||
|  | 
 | ||||||
|  |         use super::Request; | ||||||
|  |         use crate::future::send_future_message_event::unstable::FutureParameters; | ||||||
|  | 
 | ||||||
|  |         #[test] | ||||||
|  |         fn serialize_message_future_request() { | ||||||
|  |             let room_id = owned_room_id!("!roomid:example.org"); | ||||||
|  | 
 | ||||||
|  |             let req = Request::new( | ||||||
|  |                 room_id, | ||||||
|  |                 "1234".into(), | ||||||
|  |                 FutureParameters::Timeout { | ||||||
|  |                     timeout: Duration::from_millis(103), | ||||||
|  |                     group_id: Some("testId".to_owned()), | ||||||
|  |                 }, | ||||||
|  |                 &RoomMessageEventContent::text_plain("test"), | ||||||
|  |             ) | ||||||
|  |             .unwrap(); | ||||||
|  |             let request: http::Request<Vec<u8>> = req | ||||||
|  |                 .try_into_http_request( | ||||||
|  |                     "https://homeserver.tld", | ||||||
|  |                     SendAccessToken::IfRequired("auth_tok"), | ||||||
|  |                     &[MatrixVersion::V1_1], | ||||||
|  |                 ) | ||||||
|  |                 .unwrap(); | ||||||
|  |             let (parts, body) = request.into_parts(); | ||||||
|  |             assert_eq!( | ||||||
|  |                 "https://homeserver.tld/_matrix/client/unstable/org.matrix.msc4140/rooms/!roomid:example.org/send_future/m.room.message/1234?future_timeout=103&future_group_id=testId", | ||||||
|  |                 parts.uri.to_string() | ||||||
|  |             ); | ||||||
|  |             assert_eq!("PUT", parts.method.to_string()); | ||||||
|  |             assert_eq!( | ||||||
|  |                 json!({"msgtype":"m.text","body":"test"}), | ||||||
|  |                 serde_json::from_str::<JsonValue>(std::str::from_utf8(&body).unwrap()).unwrap() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										175
									
								
								crates/ruma-client-api/src/future/send_future_state_event.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								crates/ruma-client-api/src/future/send_future_state_event.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,175 @@ | |||||||
|  | //! `PUT /_matrix/client/*/rooms/{roomId}/state_future/{eventType}/{txnId}`
 | ||||||
|  | //!
 | ||||||
|  | //! Send a future state (a scheduled state event) to a room. [MSC](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)
 | ||||||
|  | 
 | ||||||
|  | pub mod unstable { | ||||||
|  |     //! `msc4140` ([MSC])
 | ||||||
|  |     //!
 | ||||||
|  |     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
 | ||||||
|  | 
 | ||||||
|  |     use ruma_common::{ | ||||||
|  |         api::{request, response, Metadata}, | ||||||
|  |         metadata, | ||||||
|  |         serde::Raw, | ||||||
|  |         OwnedRoomId, | ||||||
|  |     }; | ||||||
|  |     use ruma_events::{AnyStateEventContent, StateEventContent, StateEventType}; | ||||||
|  |     use serde_json::value::to_raw_value as to_raw_json_value; | ||||||
|  | 
 | ||||||
|  |     use crate::future::FutureParameters; | ||||||
|  | 
 | ||||||
|  |     const METADATA: Metadata = metadata! { | ||||||
|  |         method: PUT, | ||||||
|  |         rate_limited: false, | ||||||
|  |         authentication: AccessToken, | ||||||
|  |         history: { | ||||||
|  |             unstable => "/_matrix/client/unstable/org.matrix.msc4140/rooms/:room_id/state_future/:event_type/:state_key", | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /// Request type for the [`send_future_state_event`](crate::future::send_future_state_event)
 | ||||||
|  |     /// endpoint.
 | ||||||
|  |     #[request(error = crate::Error)] | ||||||
|  |     pub struct Request { | ||||||
|  |         /// The room to send the event to.
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub room_id: OwnedRoomId, | ||||||
|  | 
 | ||||||
|  |         /// The type of event to send.
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub event_type: StateEventType, | ||||||
|  | 
 | ||||||
|  |         /// The state_key for the state to send.
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub state_key: String, | ||||||
|  | 
 | ||||||
|  |         /// Additional parameters to describe sending a future.
 | ||||||
|  |         ///
 | ||||||
|  |         /// Only three combinations for `future_timeout` and `future_group_id` are possible.
 | ||||||
|  |         /// The enum [`FutureParameters`] enforces this.
 | ||||||
|  |         #[ruma_api(query_all)] | ||||||
|  |         pub future_parameters: FutureParameters, | ||||||
|  | 
 | ||||||
|  |         /// The event content to send.
 | ||||||
|  |         #[ruma_api(body)] | ||||||
|  |         pub body: Raw<AnyStateEventContent>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Response type for the [`send_future_state_event`](crate::future::send_future_state_event)
 | ||||||
|  |     /// endpoint.
 | ||||||
|  |     #[response(error = crate::Error)] | ||||||
|  |     pub struct Response { | ||||||
|  |         /// A token to send/insert the future into the DAG.
 | ||||||
|  |         pub send_token: String, | ||||||
|  |         /// A token to cancel this future. It will never be send if this is called.
 | ||||||
|  |         pub cancel_token: String, | ||||||
|  |         /// The `future_group_id` generated for this future. Used to connect multiple futures
 | ||||||
|  |         /// only one of the connected futures will be sent and inserted into the DAG.
 | ||||||
|  |         pub future_group_id: String, | ||||||
|  |         /// A token used to refresh the timer of the future. This allows
 | ||||||
|  |         /// to implement heardbeat like capabilities. An event is only send once
 | ||||||
|  |         /// a refresh in the timeout interval is missed.
 | ||||||
|  |         ///
 | ||||||
|  |         /// If the future does not have a timeout this will be `None`.
 | ||||||
|  |         pub refresh_token: Option<String>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl Request { | ||||||
|  |         /// Creates a new `Request` with the given room id, state_key future_parameters and
 | ||||||
|  |         /// event content.
 | ||||||
|  |         ///
 | ||||||
|  |         /// # Errors
 | ||||||
|  |         ///
 | ||||||
|  |         /// Since `Request` stores the request body in serialized form, this function can fail if
 | ||||||
|  |         /// `T`s [`::serde::Serialize`] implementation can fail.
 | ||||||
|  |         pub fn new<T>( | ||||||
|  |             room_id: OwnedRoomId, | ||||||
|  |             state_key: String, | ||||||
|  |             future_parameters: FutureParameters, | ||||||
|  |             content: &T, | ||||||
|  |         ) -> serde_json::Result<Self> | ||||||
|  |         where | ||||||
|  |             T: StateEventContent, | ||||||
|  |         { | ||||||
|  |             Ok(Self { | ||||||
|  |                 room_id, | ||||||
|  |                 state_key, | ||||||
|  |                 event_type: content.event_type(), | ||||||
|  |                 future_parameters, | ||||||
|  |                 body: Raw::from_json(to_raw_json_value(content)?), | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// Creates a new `Request` with the given room id, event type, state key,
 | ||||||
|  |         /// future parameters and raw event content.
 | ||||||
|  |         pub fn new_raw( | ||||||
|  |             room_id: OwnedRoomId, | ||||||
|  |             state_key: String, | ||||||
|  |             event_type: StateEventType, | ||||||
|  |             future_parameters: FutureParameters, | ||||||
|  |             body: Raw<AnyStateEventContent>, | ||||||
|  |         ) -> Self { | ||||||
|  |             Self { room_id, event_type, state_key, body, future_parameters } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl Response { | ||||||
|  |         /// Creates a new `Response` with the tokens required to control the future using the
 | ||||||
|  |         /// [`crate::future::update_future::unstable::Request`] request.
 | ||||||
|  |         pub fn new( | ||||||
|  |             send_token: String, | ||||||
|  |             cancel_token: String, | ||||||
|  |             future_group_id: String, | ||||||
|  |             refresh_token: Option<String>, | ||||||
|  |         ) -> Self { | ||||||
|  |             Self { send_token, cancel_token, future_group_id, refresh_token } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(all(test, feature = "client"))] | ||||||
|  |     mod tests { | ||||||
|  |         use ruma_common::{ | ||||||
|  |             api::{MatrixVersion, OutgoingRequest, SendAccessToken}, | ||||||
|  |             owned_room_id, | ||||||
|  |         }; | ||||||
|  |         use ruma_events::room::topic::RoomTopicEventContent; | ||||||
|  |         use serde_json::{json, Value as JsonValue}; | ||||||
|  |         use web_time::Duration; | ||||||
|  | 
 | ||||||
|  |         use super::Request; | ||||||
|  |         use crate::future::FutureParameters; | ||||||
|  | 
 | ||||||
|  |         #[test] | ||||||
|  |         fn serialize_state_future_request() { | ||||||
|  |             let room_id = owned_room_id!("!roomid:example.org"); | ||||||
|  | 
 | ||||||
|  |             let req = Request::new( | ||||||
|  |                 room_id, | ||||||
|  |                 "@userAsStateKey:example.org".to_owned(), | ||||||
|  |                 FutureParameters::Timeout { | ||||||
|  |                     timeout: Duration::from_millis(1_234_321), | ||||||
|  |                     group_id: Some("abs1abs1abs1abs1".to_owned()), | ||||||
|  |                 }, | ||||||
|  |                 &RoomTopicEventContent::new("my_topic".to_owned()), | ||||||
|  |             ) | ||||||
|  |             .unwrap(); | ||||||
|  |             let request: http::Request<Vec<u8>> = req | ||||||
|  |                 .try_into_http_request( | ||||||
|  |                     "https://homeserver.tld", | ||||||
|  |                     SendAccessToken::IfRequired("auth_tok"), | ||||||
|  |                     &[MatrixVersion::V1_1], | ||||||
|  |                 ) | ||||||
|  |                 .unwrap(); | ||||||
|  |             let (parts, body) = request.into_parts(); | ||||||
|  |             assert_eq!( | ||||||
|  |                 "https://homeserver.tld/_matrix/client/unstable/org.matrix.msc4140/rooms/!roomid:example.org/state_future/m.room.topic/@userAsStateKey:example.org?future_timeout=1234321&future_group_id=abs1abs1abs1abs1", | ||||||
|  |                 parts.uri.to_string() | ||||||
|  |             ); | ||||||
|  |             assert_eq!("PUT", parts.method.to_string()); | ||||||
|  |             assert_eq!( | ||||||
|  |                 json!({"topic": "my_topic"}), | ||||||
|  |                 serde_json::from_str::<JsonValue>(std::str::from_utf8(&body).unwrap()).unwrap() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								crates/ruma-client-api/src/future/update_future.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								crates/ruma-client-api/src/future/update_future.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | //! `POST /_matrix/client/*/futures/{token}`
 | ||||||
|  | //!
 | ||||||
|  | //! Send a future token to update/cancel/send the associated future event.
 | ||||||
|  | 
 | ||||||
|  | pub mod unstable { | ||||||
|  |     //! `msc3814` ([MSC])
 | ||||||
|  |     //!
 | ||||||
|  |     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
 | ||||||
|  | 
 | ||||||
|  |     use ruma_common::{ | ||||||
|  |         api::{request, response, Metadata}, | ||||||
|  |         metadata, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const METADATA: Metadata = metadata! { | ||||||
|  |         method: POST, | ||||||
|  |         rate_limited: true, | ||||||
|  |         authentication: None, | ||||||
|  |         history: { | ||||||
|  |             unstable => "/_matrix/client/unstable/org.matrix.msc4140/future/:token", | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /// Request type for the [`update_future`](crate::future::update_future) endpoint.
 | ||||||
|  |     #[request(error = crate::Error)] | ||||||
|  |     pub struct Request { | ||||||
|  |         /// The token.
 | ||||||
|  |         #[ruma_api(path)] | ||||||
|  |         pub token: String, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl Request { | ||||||
|  |         /// Creates a new `Request` to update a future. This is an unauthenticated request and only
 | ||||||
|  |         /// requires the future token.
 | ||||||
|  |         pub fn new(token: String) -> serde_json::Result<Self> { | ||||||
|  |             Ok(Self { token }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Response type for the [`update_future`](crate::future::update_future) endpoint.
 | ||||||
|  |     #[response(error = crate::Error)] | ||||||
|  |     pub struct Response {} | ||||||
|  |     impl Response { | ||||||
|  |         /// Creates a new response for the [`update_future`](crate::future::update_future) endpoint.
 | ||||||
|  |         pub fn new() -> Self { | ||||||
|  |             Response {} | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -12,7 +12,6 @@ | |||||||
| pub mod account; | pub mod account; | ||||||
| pub mod alias; | pub mod alias; | ||||||
| pub mod appservice; | pub mod appservice; | ||||||
| #[cfg(feature = "unstable-msc3916")] |  | ||||||
| pub mod authenticated_media; | pub mod authenticated_media; | ||||||
| pub mod backup; | pub mod backup; | ||||||
| pub mod config; | pub mod config; | ||||||
| @ -24,6 +23,8 @@ pub mod directory; | |||||||
| pub mod discovery; | pub mod discovery; | ||||||
| pub mod error; | pub mod error; | ||||||
| pub mod filter; | pub mod filter; | ||||||
|  | #[cfg(feature = "unstable-msc4140")] | ||||||
|  | pub mod future; | ||||||
| pub mod http_headers; | pub mod http_headers; | ||||||
| pub mod keys; | pub mod keys; | ||||||
| pub mod knock; | pub mod knock; | ||||||
|  | |||||||
| @ -24,11 +24,16 @@ pub mod v3 { | |||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/media/r0/download/:server_name/:media_id", |             1.0 => "/_matrix/media/r0/download/:server_name/:media_id", | ||||||
|             1.1 => "/_matrix/media/v3/download/:server_name/:media_id", |             1.1 => "/_matrix/media/v3/download/:server_name/:media_id", | ||||||
|  |             1.11 => deprecated, | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /// Request type for the `get_media_content` endpoint.
 |     /// Request type for the `get_media_content` endpoint.
 | ||||||
|     #[request(error = crate::Error)] |     #[request(error = crate::Error)] | ||||||
|  |     #[deprecated = "\ | ||||||
|  |         Since Matrix 1.11, clients should use `authenticated_media::get_content::v1::Request` \ | ||||||
|  |         instead if the homeserver supports it.\ | ||||||
|  |     "]
 | ||||||
|     pub struct Request { |     pub struct Request { | ||||||
|         /// The server name from the mxc:// URI (the authoritory component).
 |         /// The server name from the mxc:// URI (the authoritory component).
 | ||||||
|         #[ruma_api(path)] |         #[ruma_api(path)] | ||||||
| @ -106,6 +111,7 @@ pub mod v3 { | |||||||
|         pub cache_control: Option<String>, |         pub cache_control: Option<String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(deprecated)] | ||||||
|     impl Request { |     impl Request { | ||||||
|         /// Creates a new `Request` with the given media ID and server name.
 |         /// Creates a new `Request` with the given media ID and server name.
 | ||||||
|         pub fn new(media_id: String, server_name: OwnedServerName) -> Self { |         pub fn new(media_id: String, server_name: OwnedServerName) -> Self { | ||||||
|  | |||||||
| @ -24,11 +24,16 @@ pub mod v3 { | |||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/media/r0/download/:server_name/:media_id/:filename", |             1.0 => "/_matrix/media/r0/download/:server_name/:media_id/:filename", | ||||||
|             1.1 => "/_matrix/media/v3/download/:server_name/:media_id/:filename", |             1.1 => "/_matrix/media/v3/download/:server_name/:media_id/:filename", | ||||||
|  |             1.11 => deprecated, | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /// Request type for the `get_media_content_as_filename` endpoint.
 |     /// Request type for the `get_media_content_as_filename` endpoint.
 | ||||||
|     #[request(error = crate::Error)] |     #[request(error = crate::Error)] | ||||||
|  |     #[deprecated = "\ | ||||||
|  |         Since Matrix 1.11, clients should use `authenticated_media::get_content_as_filename::v1::Request` \ | ||||||
|  |         instead if the homeserver supports it.\ | ||||||
|  |     "]
 | ||||||
|     pub struct Request { |     pub struct Request { | ||||||
|         /// The server name from the mxc:// URI (the authoritory component).
 |         /// The server name from the mxc:// URI (the authoritory component).
 | ||||||
|         #[ruma_api(path)] |         #[ruma_api(path)] | ||||||
| @ -110,6 +115,7 @@ pub mod v3 { | |||||||
|         pub cache_control: Option<String>, |         pub cache_control: Option<String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(deprecated)] | ||||||
|     impl Request { |     impl Request { | ||||||
|         /// Creates a new `Request` with the given media ID, server name and filename.
 |         /// Creates a new `Request` with the given media ID, server name and filename.
 | ||||||
|         pub fn new(media_id: String, server_name: OwnedServerName, filename: String) -> Self { |         pub fn new(media_id: String, server_name: OwnedServerName, filename: String) -> Self { | ||||||
|  | |||||||
| @ -27,11 +27,16 @@ pub mod v3 { | |||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/media/r0/thumbnail/:server_name/:media_id", |             1.0 => "/_matrix/media/r0/thumbnail/:server_name/:media_id", | ||||||
|             1.1 => "/_matrix/media/v3/thumbnail/:server_name/:media_id", |             1.1 => "/_matrix/media/v3/thumbnail/:server_name/:media_id", | ||||||
|  |             1.11 => deprecated, | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /// Request type for the `get_content_thumbnail` endpoint.
 |     /// Request type for the `get_content_thumbnail` endpoint.
 | ||||||
|     #[request(error = crate::Error)] |     #[request(error = crate::Error)] | ||||||
|  |     #[deprecated = "\ | ||||||
|  |         Since Matrix 1.11, clients should use `authenticated_media::get_content_thumbnail::v1::Request` \ | ||||||
|  |         instead if the homeserver supports it.\ | ||||||
|  |     "]
 | ||||||
|     pub struct Request { |     pub struct Request { | ||||||
|         /// The server name from the mxc:// URI (the authoritory component).
 |         /// The server name from the mxc:// URI (the authoritory component).
 | ||||||
|         #[ruma_api(path)] |         #[ruma_api(path)] | ||||||
| @ -90,18 +95,12 @@ pub mod v3 { | |||||||
| 
 | 
 | ||||||
|         /// Whether the server should return an animated thumbnail.
 |         /// Whether the server should return an animated thumbnail.
 | ||||||
|         ///
 |         ///
 | ||||||
|         /// When `true`, the server should return an animated thumbnail if possible and supported.
 |         /// When `Some(true)`, the server should return an animated thumbnail if possible and
 | ||||||
|         /// Otherwise it must not return an animated thumbnail.
 |         /// supported. When `Some(false)`, the server must not return an animated
 | ||||||
|         ///
 |         /// thumbnail. When `None`, the server should not return an animated thumbnail.
 | ||||||
|         /// Defaults to `false`.
 |  | ||||||
|         #[cfg(feature = "unstable-msc2705")] |  | ||||||
|         #[ruma_api(query)] |         #[ruma_api(query)] | ||||||
|         #[serde(
 |         #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|             rename = "org.matrix.msc2705.animated", |         pub animated: Option<bool>, | ||||||
|             default, |  | ||||||
|             skip_serializing_if = "ruma_common::serde::is_default" |  | ||||||
|         )] |  | ||||||
|         pub animated: bool, |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Response type for the `get_content_thumbnail` endpoint.
 |     /// Response type for the `get_content_thumbnail` endpoint.
 | ||||||
| @ -141,6 +140,7 @@ pub mod v3 { | |||||||
|         pub content_disposition: Option<String>, |         pub content_disposition: Option<String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(deprecated)] | ||||||
|     impl Request { |     impl Request { | ||||||
|         /// Creates a new `Request` with the given media ID, server name, desired thumbnail width
 |         /// Creates a new `Request` with the given media ID, server name, desired thumbnail width
 | ||||||
|         /// and desired thumbnail height.
 |         /// and desired thumbnail height.
 | ||||||
| @ -159,8 +159,7 @@ pub mod v3 { | |||||||
|                 allow_remote: true, |                 allow_remote: true, | ||||||
|                 timeout_ms: crate::media::default_download_timeout(), |                 timeout_ms: crate::media::default_download_timeout(), | ||||||
|                 allow_redirect: false, |                 allow_redirect: false, | ||||||
|                 #[cfg(feature = "unstable-msc2705")] |                 animated: None, | ||||||
|                 animated: false, |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -20,12 +20,17 @@ pub mod v3 { | |||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/media/r0/config", |             1.0 => "/_matrix/media/r0/config", | ||||||
|             1.1 => "/_matrix/media/v3/config", |             1.1 => "/_matrix/media/v3/config", | ||||||
|  |             1.11 => deprecated, | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /// Request type for the `get_media_config` endpoint.
 |     /// Request type for the `get_media_config` endpoint.
 | ||||||
|     #[request(error = crate::Error)] |     #[request(error = crate::Error)] | ||||||
|     #[derive(Default)] |     #[derive(Default)] | ||||||
|  |     #[deprecated = "\ | ||||||
|  |         Since Matrix 1.11, clients should use `authenticated_media::get_media_config::v1::Request` \ | ||||||
|  |         instead if the homeserver supports it.\ | ||||||
|  |     "]
 | ||||||
|     pub struct Request {} |     pub struct Request {} | ||||||
| 
 | 
 | ||||||
|     /// Response type for the `get_media_config` endpoint.
 |     /// Response type for the `get_media_config` endpoint.
 | ||||||
| @ -36,6 +41,7 @@ pub mod v3 { | |||||||
|         pub upload_size: UInt, |         pub upload_size: UInt, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(deprecated)] | ||||||
|     impl Request { |     impl Request { | ||||||
|         /// Creates an empty `Request`.
 |         /// Creates an empty `Request`.
 | ||||||
|         pub fn new() -> Self { |         pub fn new() -> Self { | ||||||
|  | |||||||
| @ -21,11 +21,16 @@ pub mod v3 { | |||||||
|         history: { |         history: { | ||||||
|             1.0 => "/_matrix/media/r0/preview_url", |             1.0 => "/_matrix/media/r0/preview_url", | ||||||
|             1.1 => "/_matrix/media/v3/preview_url", |             1.1 => "/_matrix/media/v3/preview_url", | ||||||
|  |             1.11 => deprecated, | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /// Request type for the `get_media_preview` endpoint.
 |     /// Request type for the `get_media_preview` endpoint.
 | ||||||
|     #[request(error = crate::Error)] |     #[request(error = crate::Error)] | ||||||
|  |     #[deprecated = "\ | ||||||
|  |       Since Matrix 1.11, clients should use `authenticated_media::get_media_preview::v1::Request` \ | ||||||
|  |       instead if the homeserver supports it.\ | ||||||
|  |     "]
 | ||||||
|     pub struct Request { |     pub struct Request { | ||||||
|         /// URL to get a preview of.
 |         /// URL to get a preview of.
 | ||||||
|         #[ruma_api(query)] |         #[ruma_api(query)] | ||||||
| @ -49,6 +54,7 @@ pub mod v3 { | |||||||
|         pub data: Option<Box<RawJsonValue>>, |         pub data: Option<Box<RawJsonValue>>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(deprecated)] | ||||||
|     impl Request { |     impl Request { | ||||||
|         /// Creates a new `Request` with the given url.
 |         /// Creates a new `Request` with the given url.
 | ||||||
|         pub fn new(url: String) -> Self { |         pub fn new(url: String) -> Self { | ||||||
|  | |||||||
| @ -9,8 +9,8 @@ pub mod v3 { | |||||||
|     //! [by their Matrix identifier][spec-mxid], and one to invite a user
 |     //! [by their Matrix identifier][spec-mxid], and one to invite a user
 | ||||||
|     //! [by their third party identifier][spec-3pid].
 |     //! [by their third party identifier][spec-3pid].
 | ||||||
|     //!
 |     //!
 | ||||||
|     //! [spec-mxid]: https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3roomsroomidinvite
 |     //! [spec-mxid]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3roomsroomidinvite
 | ||||||
|     //! [spec-3pid]: https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3roomsroomidinvite-1
 |     //! [spec-3pid]: https://spec.matrix.org/latest/client-server-api/#thirdparty_post_matrixclientv3roomsroomidinvite
 | ||||||
| 
 | 
 | ||||||
|     use ruma_common::{ |     use ruma_common::{ | ||||||
|         api::{request, response, Metadata}, |         api::{request, response, Metadata}, | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ pub mod v3 { | |||||||
| 
 | 
 | ||||||
|         /// One or more custom fields to help identify the third party location.
 |         /// One or more custom fields to help identify the third party location.
 | ||||||
|         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 |         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 | ||||||
|         #[ruma_api(query_map)] |         #[ruma_api(query_all)] | ||||||
|         pub fields: BTreeMap<String, String>, |         pub fields: BTreeMap<String, String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ pub mod v3 { | |||||||
| 
 | 
 | ||||||
|         /// One or more custom fields that are passed to the AS to help identify the user.
 |         /// One or more custom fields that are passed to the AS to help identify the user.
 | ||||||
|         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 |         // The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
 | ||||||
|         #[ruma_api(query_map)] |         #[ruma_api(query_all)] | ||||||
|         pub fields: BTreeMap<String, String>, |         pub fields: BTreeMap<String, String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -53,6 +53,11 @@ pub enum AuthData { | |||||||
|     /// Fallback acknowledgement.
 |     /// Fallback acknowledgement.
 | ||||||
|     FallbackAcknowledgement(FallbackAcknowledgement), |     FallbackAcknowledgement(FallbackAcknowledgement), | ||||||
| 
 | 
 | ||||||
|  |     /// Terms of service (`m.login.terms`).
 | ||||||
|  |     ///
 | ||||||
|  |     /// This type is only valid during account registration.
 | ||||||
|  |     Terms(Terms), | ||||||
|  | 
 | ||||||
|     #[doc(hidden)] |     #[doc(hidden)] | ||||||
|     _Custom(CustomAuthData), |     _Custom(CustomAuthData), | ||||||
| } | } | ||||||
| @ -90,6 +95,7 @@ impl AuthData { | |||||||
|             "m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?), |             "m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?), | ||||||
|             "m.login.dummy" => Self::Dummy(deserialize_variant(session, data)?), |             "m.login.dummy" => Self::Dummy(deserialize_variant(session, data)?), | ||||||
|             "m.registration_token" => Self::RegistrationToken(deserialize_variant(session, data)?), |             "m.registration_token" => Self::RegistrationToken(deserialize_variant(session, data)?), | ||||||
|  |             "m.login.terms" => Self::Terms(deserialize_variant(session, data)?), | ||||||
|             _ => { |             _ => { | ||||||
|                 Self::_Custom(CustomAuthData { auth_type: auth_type.into(), session, extra: data }) |                 Self::_Custom(CustomAuthData { auth_type: auth_type.into(), session, extra: data }) | ||||||
|             } |             } | ||||||
| @ -111,6 +117,7 @@ impl AuthData { | |||||||
|             Self::Dummy(_) => Some(AuthType::Dummy), |             Self::Dummy(_) => Some(AuthType::Dummy), | ||||||
|             Self::RegistrationToken(_) => Some(AuthType::RegistrationToken), |             Self::RegistrationToken(_) => Some(AuthType::RegistrationToken), | ||||||
|             Self::FallbackAcknowledgement(_) => None, |             Self::FallbackAcknowledgement(_) => None, | ||||||
|  |             Self::Terms(_) => Some(AuthType::Terms), | ||||||
|             Self::_Custom(c) => Some(AuthType::_Custom(PrivOwnedStr(c.auth_type.as_str().into()))), |             Self::_Custom(c) => Some(AuthType::_Custom(PrivOwnedStr(c.auth_type.as_str().into()))), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -125,6 +132,7 @@ impl AuthData { | |||||||
|             Self::Dummy(x) => x.session.as_deref(), |             Self::Dummy(x) => x.session.as_deref(), | ||||||
|             Self::RegistrationToken(x) => x.session.as_deref(), |             Self::RegistrationToken(x) => x.session.as_deref(), | ||||||
|             Self::FallbackAcknowledgement(x) => Some(&x.session), |             Self::FallbackAcknowledgement(x) => Some(&x.session), | ||||||
|  |             Self::Terms(x) => x.session.as_deref(), | ||||||
|             Self::_Custom(x) => x.session.as_deref(), |             Self::_Custom(x) => x.session.as_deref(), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -165,8 +173,10 @@ impl AuthData { | |||||||
|             Self::RegistrationToken(x) => { |             Self::RegistrationToken(x) => { | ||||||
|                 Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None })) |                 Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None })) | ||||||
|             } |             } | ||||||
|             // Dummy and fallback acknowledgement have no associated data
 |             // Dummy, fallback acknowledgement, and terms of service have no associated data
 | ||||||
|             Self::Dummy(_) | Self::FallbackAcknowledgement(_) => Cow::Owned(JsonObject::default()), |             Self::Dummy(_) | Self::FallbackAcknowledgement(_) | Self::Terms(_) => { | ||||||
|  |                 Cow::Owned(JsonObject::default()) | ||||||
|  |             } | ||||||
|             Self::_Custom(c) => Cow::Borrowed(&c.extra), |             Self::_Custom(c) => Cow::Borrowed(&c.extra), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -183,6 +193,7 @@ impl fmt::Debug for AuthData { | |||||||
|             Self::Dummy(inner) => inner.fmt(f), |             Self::Dummy(inner) => inner.fmt(f), | ||||||
|             Self::RegistrationToken(inner) => inner.fmt(f), |             Self::RegistrationToken(inner) => inner.fmt(f), | ||||||
|             Self::FallbackAcknowledgement(inner) => inner.fmt(f), |             Self::FallbackAcknowledgement(inner) => inner.fmt(f), | ||||||
|  |             Self::Terms(inner) => inner.fmt(f), | ||||||
|             Self::_Custom(inner) => inner.fmt(f), |             Self::_Custom(inner) => inner.fmt(f), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -214,6 +225,7 @@ impl<'de> Deserialize<'de> for AuthData { | |||||||
|             Some("m.login.registration_token") => { |             Some("m.login.registration_token") => { | ||||||
|                 from_raw_json_value(&json).map(Self::RegistrationToken) |                 from_raw_json_value(&json).map(Self::RegistrationToken) | ||||||
|             } |             } | ||||||
|  |             Some("m.login.terms") => from_raw_json_value(&json).map(Self::Terms), | ||||||
|             None => from_raw_json_value(&json).map(Self::FallbackAcknowledgement), |             None => from_raw_json_value(&json).map(Self::FallbackAcknowledgement), | ||||||
|             Some(_) => from_raw_json_value(&json).map(Self::_Custom), |             Some(_) => from_raw_json_value(&json).map(Self::_Custom), | ||||||
|         } |         } | ||||||
| @ -253,6 +265,12 @@ pub enum AuthType { | |||||||
|     #[ruma_enum(rename = "m.login.registration_token")] |     #[ruma_enum(rename = "m.login.registration_token")] | ||||||
|     RegistrationToken, |     RegistrationToken, | ||||||
| 
 | 
 | ||||||
|  |     /// Terms of service (`m.login.terms`).
 | ||||||
|  |     ///
 | ||||||
|  |     /// This type is only valid during account registration.
 | ||||||
|  |     #[ruma_enum(rename = "m.login.terms")] | ||||||
|  |     Terms, | ||||||
|  | 
 | ||||||
|     #[doc(hidden)] |     #[doc(hidden)] | ||||||
|     _Custom(PrivOwnedStr), |     _Custom(PrivOwnedStr), | ||||||
| } | } | ||||||
| @ -434,6 +452,28 @@ impl FallbackAcknowledgement { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Data for terms of service flow.
 | ||||||
|  | ///
 | ||||||
|  | /// This type is only valid during account registration.
 | ||||||
|  | ///
 | ||||||
|  | /// See [the spec] for how to use this.
 | ||||||
|  | ///
 | ||||||
|  | /// [the spec]: https://spec.matrix.org/latest/client-server-api/#terms-of-service-at-registration
 | ||||||
|  | #[derive(Clone, Debug, Default, Deserialize, Serialize)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[serde(tag = "type", rename = "m.login.terms")] | ||||||
|  | pub struct Terms { | ||||||
|  |     /// The value of the session key given by the homeserver, if any.
 | ||||||
|  |     pub session: Option<String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Terms { | ||||||
|  |     /// Creates an empty `Terms`.
 | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self::default() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[doc(hidden)] | #[doc(hidden)] | ||||||
| #[derive(Clone, Deserialize, Serialize)] | #[derive(Clone, Deserialize, Serialize)] | ||||||
| #[non_exhaustive] | #[non_exhaustive] | ||||||
| @ -551,29 +591,25 @@ pub struct IncomingCustomThirdPartyId { | |||||||
| #[derive(Clone, Deserialize, Serialize)] | #[derive(Clone, Deserialize, Serialize)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| pub struct ThirdpartyIdCredentials { | pub struct ThirdpartyIdCredentials { | ||||||
|     /// Identity server session ID.
 |     /// Identity server (or homeserver) session ID.
 | ||||||
|     pub sid: OwnedSessionId, |     pub sid: OwnedSessionId, | ||||||
| 
 | 
 | ||||||
|     /// Identity server client secret.
 |     /// Identity server (or homeserver) client secret.
 | ||||||
|     pub client_secret: OwnedClientSecret, |     pub client_secret: OwnedClientSecret, | ||||||
| 
 | 
 | ||||||
|     /// Identity server URL.
 |     /// Identity server URL.
 | ||||||
|     pub id_server: String, |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub id_server: Option<String>, | ||||||
| 
 | 
 | ||||||
|     /// Identity server access token.
 |     /// Identity server access token.
 | ||||||
|     pub id_access_token: String, |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub id_access_token: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl ThirdpartyIdCredentials { | impl ThirdpartyIdCredentials { | ||||||
|     /// Creates a new `ThirdpartyIdCredentials` with the given session ID, client secret, identity
 |     /// Creates a new `ThirdpartyIdCredentials` with the given session ID and client secret.
 | ||||||
|     /// server address and access token.
 |     pub fn new(sid: OwnedSessionId, client_secret: OwnedClientSecret) -> Self { | ||||||
|     pub fn new( |         Self { sid, client_secret, id_server: None, id_access_token: None } | ||||||
|         sid: OwnedSessionId, |  | ||||||
|         client_secret: OwnedClientSecret, |  | ||||||
|         id_server: String, |  | ||||||
|         id_access_token: String, |  | ||||||
|     ) -> Self { |  | ||||||
|         Self { sid, client_secret, id_server, id_access_token } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,9 +1,30 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | Bug fixes: | ||||||
|  | 
 | ||||||
|  | - The `instance_id` field was removed from `ProtocolInstanceInit` and is now an | ||||||
|  |   `Option<String>` for `ProtocolInstance`. It made the `unstable-unspecified` | ||||||
|  |   feature non-additive. | ||||||
|  | 
 | ||||||
|  | Breaking changes: | ||||||
|  | 
 | ||||||
|  | - Rename the `query_map` attribute of the `request` macro to `query_all`, and | ||||||
|  |   remove the required bound to implement `IntoIterator<Item = (String, String)>`. | ||||||
|  |   This allows to use a struct or enum as well as a map to represent the list of | ||||||
|  |   query parameters. Note that the (de)serialization of the type used must work | ||||||
|  |   with `serde_html_form`. | ||||||
|  | 
 | ||||||
| Improvements: | Improvements: | ||||||
| 
 | 
 | ||||||
| - Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for | - Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for | ||||||
|   cases where we receive a HTTP header with an unexpected value. |   cases where we receive a HTTP header with an unexpected value. | ||||||
|  | - Implement `Eq`/`Hash`/`PartialEq` for `ThirdPartyIdentifier`, to check whether  | ||||||
|  |   a `ThirdPartyIdentifier` has already been added by another user. | ||||||
|  | - Add `MatrixVersion::V1_11` | ||||||
|  | - Clarify in the docs of `AuthScheme` that sending an access token via a query | ||||||
|  |   parameter is deprecated, according to MSC4126 / Matrix 1.11. | ||||||
|  | - Constructing a Matrix URI for an event with a room alias is deprecated, | ||||||
|  |   according to MSC4132 / Matrix 1.11 | ||||||
| 
 | 
 | ||||||
| # 0.13.0 | # 0.13.0 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -122,9 +122,11 @@ macro_rules! metadata { | |||||||
| ///   they are declared must match the order in which they occur in the request path.
 | ///   they are declared must match the order in which they occur in the request path.
 | ||||||
| /// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
 | /// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
 | ||||||
| ///   string.
 | ///   string.
 | ||||||
| /// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any
 | /// * `#[ruma_api(query_all)]`: Instead of individual query fields, one query_all field, of any
 | ||||||
| ///   type that implements `IntoIterator<Item = (String, String)>` (e.g. `HashMap<String,
 | ///   type that can be (de)serialized by [serde_html_form], can be used for cases where
 | ||||||
| ///   String>`, can be used for cases where an endpoint supports arbitrary query parameters.
 | ///   multiple endpoints should share a query fields type, the query fields are better
 | ||||||
|  | ///   expressed as an `enum` rather than a `struct`, or the endpoint supports arbitrary query
 | ||||||
|  | ///   parameters.
 | ||||||
| /// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
 | /// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
 | ||||||
| ///   attributes to customize (de)serialization.
 | ///   attributes to customize (de)serialization.
 | ||||||
| /// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
 | /// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
 | ||||||
| @ -209,11 +211,13 @@ macro_rules! metadata { | |||||||
| ///     # pub struct Response {}
 | ///     # pub struct Response {}
 | ||||||
| /// }
 | /// }
 | ||||||
| /// ```
 | /// ```
 | ||||||
|  | ///
 | ||||||
|  | /// [serde_html_form]: https://crates.io/crates/serde_html_form
 | ||||||
| pub use ruma_macros::request; | pub use ruma_macros::request; | ||||||
| /// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
 | /// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
 | ||||||
| ///
 | ///
 | ||||||
| /// The `OutgoingRequest` impl is feature-gated behind `cfg(feature = "client")`.
 | /// The `OutgoingResponse` impl is feature-gated behind `cfg(feature = "server")`.
 | ||||||
| /// The `IncomingRequest` impl is feature-gated behind `cfg(feature = "server")`.
 | /// The `IncomingResponse` impl is feature-gated behind `cfg(feature = "client")`.
 | ||||||
| ///
 | ///
 | ||||||
| /// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope.
 | /// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope.
 | ||||||
| ///
 | ///
 | ||||||
| @ -223,7 +227,7 @@ pub use ruma_macros::request; | |||||||
| ///
 | ///
 | ||||||
| /// ## Attributes
 | /// ## Attributes
 | ||||||
| ///
 | ///
 | ||||||
| /// To declare which part of the request a field belongs to:
 | /// To declare which part of the response a field belongs to:
 | ||||||
| ///
 | ///
 | ||||||
| /// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
 | /// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
 | ||||||
| ///   headers on the response. The value must implement `Display`. Generally this is a
 | ///   headers on the response. The value must implement `Display`. Generally this is a
 | ||||||
| @ -497,19 +501,19 @@ pub enum AuthScheme { | |||||||
|     /// Authentication is performed by including an access token in the `Authentication` http
 |     /// Authentication is performed by including an access token in the `Authentication` http
 | ||||||
|     /// header, or an `access_token` query parameter.
 |     /// header, or an `access_token` query parameter.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// It is recommended to use the header over the query parameter.
 |     /// Using the query parameter is deprecated since Matrix 1.11.
 | ||||||
|     AccessToken, |     AccessToken, | ||||||
| 
 | 
 | ||||||
|     /// Authentication is optional, and it is performed by including an access token in the
 |     /// Authentication is optional, and it is performed by including an access token in the
 | ||||||
|     /// `Authentication` http header, or an `access_token` query parameter.
 |     /// `Authentication` http header, or an `access_token` query parameter.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// It is recommended to use the header over the query parameter.
 |     /// Using the query parameter is deprecated since Matrix 1.11.
 | ||||||
|     AccessTokenOptional, |     AccessTokenOptional, | ||||||
| 
 | 
 | ||||||
|     /// Authentication is only performed for appservices, by including an access token in the
 |     /// Authentication is only performed for appservices, by including an access token in the
 | ||||||
|     /// `Authentication` http header, or an `access_token` query parameter.
 |     /// `Authentication` http header, or an `access_token` query parameter.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// It is recommended to use the header over the query parameter.
 |     /// Using the query parameter is deprecated since Matrix 1.11.
 | ||||||
|     AppserviceToken, |     AppserviceToken, | ||||||
| 
 | 
 | ||||||
|     /// Authentication is performed by including X-Matrix signatures in the request headers,
 |     /// Authentication is performed by including X-Matrix signatures in the request headers,
 | ||||||
|  | |||||||
| @ -541,6 +541,11 @@ pub enum MatrixVersion { | |||||||
|     ///
 |     ///
 | ||||||
|     /// See <https://spec.matrix.org/v1.10/>.
 |     /// See <https://spec.matrix.org/v1.10/>.
 | ||||||
|     V1_10, |     V1_10, | ||||||
|  | 
 | ||||||
|  |     /// Version 1.11 of the Matrix specification, released in Q2 2024.
 | ||||||
|  |     ///
 | ||||||
|  |     /// See <https://spec.matrix.org/v1.11/>.
 | ||||||
|  |     V1_11, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl TryFrom<&str> for MatrixVersion { | impl TryFrom<&str> for MatrixVersion { | ||||||
| @ -564,6 +569,7 @@ impl TryFrom<&str> for MatrixVersion { | |||||||
|             "v1.8" => V1_8, |             "v1.8" => V1_8, | ||||||
|             "v1.9" => V1_9, |             "v1.9" => V1_9, | ||||||
|             "v1.10" => V1_10, |             "v1.10" => V1_10, | ||||||
|  |             "v1.11" => V1_11, | ||||||
|             _ => return Err(UnknownVersionError), |             _ => return Err(UnknownVersionError), | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| @ -613,6 +619,7 @@ impl MatrixVersion { | |||||||
|             MatrixVersion::V1_8 => (1, 8), |             MatrixVersion::V1_8 => (1, 8), | ||||||
|             MatrixVersion::V1_9 => (1, 9), |             MatrixVersion::V1_9 => (1, 9), | ||||||
|             MatrixVersion::V1_10 => (1, 10), |             MatrixVersion::V1_10 => (1, 10), | ||||||
|  |             MatrixVersion::V1_11 => (1, 11), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -630,6 +637,7 @@ impl MatrixVersion { | |||||||
|             (1, 8) => Ok(MatrixVersion::V1_8), |             (1, 8) => Ok(MatrixVersion::V1_8), | ||||||
|             (1, 9) => Ok(MatrixVersion::V1_9), |             (1, 9) => Ok(MatrixVersion::V1_9), | ||||||
|             (1, 10) => Ok(MatrixVersion::V1_10), |             (1, 10) => Ok(MatrixVersion::V1_10), | ||||||
|  |             (1, 11) => Ok(MatrixVersion::V1_11), | ||||||
|             _ => Err(UnknownVersionError), |             _ => Err(UnknownVersionError), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -724,7 +732,9 @@ impl MatrixVersion { | |||||||
|             // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
 |             // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
 | ||||||
|             | MatrixVersion::V1_9 |             | MatrixVersion::V1_9 | ||||||
|             // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
 |             // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
 | ||||||
|             | MatrixVersion::V1_10 => RoomVersionId::V10, |             | MatrixVersion::V1_10 | ||||||
|  |             // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
 | ||||||
|  |             | MatrixVersion::V1_11  => RoomVersionId::V10, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -32,6 +32,9 @@ pub enum MatrixId { | |||||||
|     User(OwnedUserId), |     User(OwnedUserId), | ||||||
| 
 | 
 | ||||||
|     /// An event ID.
 |     /// An event ID.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Constructing this variant from an `OwnedRoomAliasId` is deprecated, because room aliases
 | ||||||
|  |     /// are mutable, so the URI might break after a while.
 | ||||||
|     Event(OwnedRoomOrAliasId, OwnedEventId), |     Event(OwnedRoomOrAliasId, OwnedEventId), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -572,12 +575,11 @@ mod tests { | |||||||
|                 .to_string(), |                 .to_string(), | ||||||
|             "https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs" |             "https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs" | ||||||
|         ); |         ); | ||||||
|         assert_eq!( |         #[allow(deprecated)] | ||||||
|             room_alias_id!("#ruma:notareal.hs") |         let uri = room_alias_id!("#ruma:notareal.hs") | ||||||
|                 .matrix_to_event_uri(event_id!("$event:notareal.hs")) |             .matrix_to_event_uri(event_id!("$event:notareal.hs")) | ||||||
|                 .to_string(), |             .to_string(); | ||||||
|             "https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs" |         assert_eq!(uri, "https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs"); | ||||||
|         ); |  | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             room_id!("!ruma:notareal.hs") |             room_id!("!ruma:notareal.hs") | ||||||
|                 .matrix_to_event_uri(event_id!("$event:notareal.hs")) |                 .matrix_to_event_uri(event_id!("$event:notareal.hs")) | ||||||
| @ -869,12 +871,11 @@ mod tests { | |||||||
|                 .to_string(), |                 .to_string(), | ||||||
|             "matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join" |             "matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join" | ||||||
|         ); |         ); | ||||||
|         assert_eq!( |         #[allow(deprecated)] | ||||||
|             room_alias_id!("#ruma:notareal.hs") |         let uri = room_alias_id!("#ruma:notareal.hs") | ||||||
|                 .matrix_event_uri(event_id!("$event:notareal.hs")) |             .matrix_event_uri(event_id!("$event:notareal.hs")) | ||||||
|                 .to_string(), |             .to_string(); | ||||||
|             "matrix:r/ruma:notareal.hs/e/event:notareal.hs" |         assert_eq!(uri, "matrix:r/ruma:notareal.hs/e/event:notareal.hs"); | ||||||
|         ); |  | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             room_id!("!ruma:notareal.hs") |             room_id!("!ruma:notareal.hs") | ||||||
|                 .matrix_event_uri(event_id!("$event:notareal.hs")) |                 .matrix_event_uri(event_id!("$event:notareal.hs")) | ||||||
|  | |||||||
| @ -37,6 +37,9 @@ impl RoomAliasId { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Create a `matrix.to` URI for an event scoped under this room alias ID.
 |     /// Create a `matrix.to` URI for an event scoped under this room alias ID.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is deprecated because room aliases are mutable, so the URI might break after a while.
 | ||||||
|  |     #[deprecated = "Use `RoomId::matrix_to_event_uri` instead."] | ||||||
|     pub fn matrix_to_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixToUri { |     pub fn matrix_to_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixToUri { | ||||||
|         MatrixToUri::new((self.to_owned(), ev_id.into()).into(), Vec::new()) |         MatrixToUri::new((self.to_owned(), ev_id.into()).into(), Vec::new()) | ||||||
|     } |     } | ||||||
| @ -49,6 +52,9 @@ impl RoomAliasId { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Create a `matrix:` URI for an event scoped under this room alias ID.
 |     /// Create a `matrix:` URI for an event scoped under this room alias ID.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is deprecated because room aliases are mutable, so the URI might break after a while.
 | ||||||
|  |     #[deprecated = "Use `RoomId::matrix_event_uri` instead."] | ||||||
|     pub fn matrix_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixUri { |     pub fn matrix_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixUri { | ||||||
|         MatrixUri::new((self.to_owned(), ev_id.into()).into(), Vec::new(), None) |         MatrixUri::new((self.to_owned(), ev_id.into()).into(), Vec::new(), None) | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -602,7 +602,7 @@ mod tests { | |||||||
|         assert!(!"m".matches_word("[[:alpha:]]?")); |         assert!(!"m".matches_word("[[:alpha:]]?")); | ||||||
|         assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?")); |         assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?")); | ||||||
| 
 | 
 | ||||||
|         // From the spec: <https://spec.matrix.org/v1.10/client-server-api/#conditions-1>
 |         // From the spec: <https://spec.matrix.org/v1.11/client-server-api/#conditions-1>
 | ||||||
|         assert!("An example event.".matches_word("ex*ple")); |         assert!("An example event.".matches_word("ex*ple")); | ||||||
|         assert!("exple".matches_word("ex*ple")); |         assert!("exple".matches_word("ex*ple")); | ||||||
|         assert!("An exciting triple-whammy".matches_word("ex*ple")); |         assert!("An exciting triple-whammy".matches_word("ex*ple")); | ||||||
| @ -651,7 +651,7 @@ mod tests { | |||||||
|         assert!("".matches_pattern("*", false)); |         assert!("".matches_pattern("*", false)); | ||||||
|         assert!(!"foo".matches_pattern("", false)); |         assert!(!"foo".matches_pattern("", false)); | ||||||
| 
 | 
 | ||||||
|         // From the spec: <https://spec.matrix.org/v1.10/client-server-api/#conditions-1>
 |         // From the spec: <https://spec.matrix.org/v1.11/client-server-api/#conditions-1>
 | ||||||
|         assert!("Lunch plans".matches_pattern("lunc?*", false)); |         assert!("Lunch plans".matches_pattern("lunc?*", false)); | ||||||
|         assert!("LUNCH".matches_pattern("lunc?*", false)); |         assert!("LUNCH".matches_pattern("lunc?*", false)); | ||||||
|         assert!(!" lunch".matches_pattern("lunc?*", false)); |         assert!(!" lunch".matches_pattern("lunc?*", false)); | ||||||
|  | |||||||
| @ -2,7 +2,10 @@ | |||||||
| //!
 | //!
 | ||||||
| //! [thirdparty]: https://spec.matrix.org/latest/client-server-api/#third-party-networks
 | //! [thirdparty]: https://spec.matrix.org/latest/client-server-api/#third-party-networks
 | ||||||
| 
 | 
 | ||||||
| use std::collections::BTreeMap; | use std::{ | ||||||
|  |     collections::BTreeMap, | ||||||
|  |     hash::{Hash, Hasher}, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| 
 | 
 | ||||||
| @ -91,7 +94,8 @@ pub struct ProtocolInstance { | |||||||
|     ///
 |     ///
 | ||||||
|     /// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
 |     /// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
 | ||||||
|     #[cfg(feature = "unstable-unspecified")] |     #[cfg(feature = "unstable-unspecified")] | ||||||
|     pub instance_id: String, |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub instance_id: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Initial set of fields of `Protocol`.
 | /// Initial set of fields of `Protocol`.
 | ||||||
| @ -109,30 +113,18 @@ pub struct ProtocolInstanceInit { | |||||||
| 
 | 
 | ||||||
|     /// A unique identifier across all instances.
 |     /// A unique identifier across all instances.
 | ||||||
|     pub network_id: String, |     pub network_id: String, | ||||||
| 
 |  | ||||||
|     /// A unique identifier across all instances.
 |  | ||||||
|     ///
 |  | ||||||
|     /// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
 |  | ||||||
|     #[cfg(feature = "unstable-unspecified")] |  | ||||||
|     pub instance_id: String, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl From<ProtocolInstanceInit> for ProtocolInstance { | impl From<ProtocolInstanceInit> for ProtocolInstance { | ||||||
|     fn from(init: ProtocolInstanceInit) -> Self { |     fn from(init: ProtocolInstanceInit) -> Self { | ||||||
|         let ProtocolInstanceInit { |         let ProtocolInstanceInit { desc, fields, network_id } = init; | ||||||
|             desc, |  | ||||||
|             fields, |  | ||||||
|             network_id, |  | ||||||
|             #[cfg(feature = "unstable-unspecified")] |  | ||||||
|             instance_id, |  | ||||||
|         } = init; |  | ||||||
|         Self { |         Self { | ||||||
|             desc, |             desc, | ||||||
|             icon: None, |             icon: None, | ||||||
|             fields, |             fields, | ||||||
|             network_id, |             network_id, | ||||||
|             #[cfg(feature = "unstable-unspecified")] |             #[cfg(feature = "unstable-unspecified")] | ||||||
|             instance_id, |             instance_id: None, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -240,7 +232,6 @@ pub enum Medium { | |||||||
| /// this type using `ThirdPartyIdentifier::Init` / `.into()`.
 | /// this type using `ThirdPartyIdentifier::Init` / `.into()`.
 | ||||||
| #[derive(Clone, Debug, Deserialize, Serialize)] | #[derive(Clone, Debug, Deserialize, Serialize)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| #[cfg_attr(test, derive(PartialEq))] |  | ||||||
| pub struct ThirdPartyIdentifier { | pub struct ThirdPartyIdentifier { | ||||||
|     /// The third party identifier address.
 |     /// The third party identifier address.
 | ||||||
|     pub address: String, |     pub address: String, | ||||||
| @ -255,6 +246,20 @@ pub struct ThirdPartyIdentifier { | |||||||
|     pub added_at: MilliSecondsSinceUnixEpoch, |     pub added_at: MilliSecondsSinceUnixEpoch, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl Eq for ThirdPartyIdentifier {} | ||||||
|  | 
 | ||||||
|  | impl Hash for ThirdPartyIdentifier { | ||||||
|  |     fn hash<H: Hasher>(&self, hasher: &mut H) { | ||||||
|  |         (self.medium.as_str(), &self.address).hash(hasher); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl PartialEq for ThirdPartyIdentifier { | ||||||
|  |     fn eq(&self, other: &ThirdPartyIdentifier) -> bool { | ||||||
|  |         self.address == other.address && self.medium == other.medium | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /// Initial set of fields of `ThirdPartyIdentifier`.
 | /// Initial set of fields of `ThirdPartyIdentifier`.
 | ||||||
| ///
 | ///
 | ||||||
| /// This struct will not be updated even if additional fields are added to `ThirdPartyIdentifier`
 | /// This struct will not be updated even if additional fields are added to `ThirdPartyIdentifier`
 | ||||||
|  | |||||||
| @ -133,7 +133,41 @@ pub mod raw_body_endpoint { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub mod query_map_endpoint { | pub mod query_all_enum_endpoint { | ||||||
|  |     use ruma_common::{ | ||||||
|  |         api::{request, response, Metadata}, | ||||||
|  |         metadata, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] | ||||||
|  |     #[serde(untagged)] | ||||||
|  |     pub enum MyCustomQueryEnum { | ||||||
|  |         VariantA { field_a: String }, | ||||||
|  |         VariantB { field_b: String }, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const METADATA: Metadata = metadata! { | ||||||
|  |         method: GET, | ||||||
|  |         rate_limited: false, | ||||||
|  |         authentication: None, | ||||||
|  |         history: { | ||||||
|  |             unstable => "/_matrix/some/query/map/endpoint", | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /// Request type for the `query_all_enum_endpoint` endpoint.
 | ||||||
|  |     #[request] | ||||||
|  |     pub struct Request { | ||||||
|  |         #[ruma_api(query_all)] | ||||||
|  |         pub query: MyCustomQueryEnum, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Response type for the `query_all_enum_endpoint` endpoint.
 | ||||||
|  |     #[response] | ||||||
|  |     pub struct Response {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub mod query_all_vec_endpoint { | ||||||
|     use ruma_common::{ |     use ruma_common::{ | ||||||
|         api::{request, response, Metadata}, |         api::{request, response, Metadata}, | ||||||
|         metadata, |         metadata, | ||||||
| @ -148,14 +182,14 @@ pub mod query_map_endpoint { | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /// Request type for the `newtype_body_endpoint` endpoint.
 |     /// Request type for the `query_all_vec_endpoint` endpoint.
 | ||||||
|     #[request] |     #[request] | ||||||
|     pub struct Request { |     pub struct Request { | ||||||
|         #[ruma_api(query_map)] |         #[ruma_api(query_all)] | ||||||
|         pub fields: Vec<(String, String)>, |         pub fields: Vec<(String, String)>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Response type for the `newtype_body_endpoint` endpoint.
 |     /// Response type for the `query_all_vec_endpoint` endpoint.
 | ||||||
|     #[response] |     #[response] | ||||||
|     pub struct Response {} |     pub struct Response {} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,12 +1,25 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | Bug fixes: | ||||||
|  | 
 | ||||||
|  | - Fix deserialization of `AnyGlobalAccountDataEvent` for variants with a type | ||||||
|  |   fragment. | ||||||
|  | - Fix serialization of `room::message::Relation` and `room::encrypted::Relation` | ||||||
|  |   which could cause duplicate `rel_type` keys.  | ||||||
|  | - `Restricted` no longer fails to deserialize when the `allow` field is missing | ||||||
|  | 
 | ||||||
| Improvements: | Improvements: | ||||||
| 
 | 
 | ||||||
|  - Add support for encrypted stickers as sent by several bridges under the flag `compat-encrypted-stickers` | - Add support for encrypted stickers as sent by several bridges under the flag `compat-encrypted-stickers` | ||||||
|  | - Add unstable support for MSC3489 `m.beacon` & `m.beacon_info` events | ||||||
|  |   (unstable types `org.matrix.msc3489.beacon` & `org.matrix.msc3489.beacon_info`) | ||||||
|  | - 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. | ||||||
| 
 | 
 | ||||||
| Breaking changes: | Breaking changes: | ||||||
| 
 | 
 | ||||||
|  - `StickerEventContent::url` was replaced by `StickerEventContent::source` which is a `StickerMediaSource` | - `StickerEventContent::url` was replaced by `StickerEventContent::source` which is a `StickerMediaSource` | ||||||
| 
 | 
 | ||||||
| # 0.28.1 | # 0.28.1 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ all-features = true | |||||||
| [features] | [features] | ||||||
| canonical-json = ["ruma-common/canonical-json"] | canonical-json = ["ruma-common/canonical-json"] | ||||||
| html = ["dep:ruma-html"] | html = ["dep:ruma-html"] | ||||||
| markdown = ["pulldown-cmark"] | markdown = ["dep:pulldown-cmark"] | ||||||
| unstable-exhaustive-types = [] | unstable-exhaustive-types = [] | ||||||
| unstable-msc1767 = [] | unstable-msc1767 = [] | ||||||
| unstable-msc2448 = [] | unstable-msc2448 = [] | ||||||
| @ -29,10 +29,10 @@ unstable-msc3245 = ["unstable-msc3246"] | |||||||
| # https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md | # https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md | ||||||
| unstable-msc3245-v1-compat = [] | unstable-msc3245-v1-compat = [] | ||||||
| unstable-msc3246 = ["unstable-msc3927"] | unstable-msc3246 = ["unstable-msc3927"] | ||||||
| unstable-msc3291 = [] |  | ||||||
| unstable-msc3381 = ["unstable-msc1767"] | unstable-msc3381 = ["unstable-msc1767"] | ||||||
| unstable-msc3401 = [] | unstable-msc3401 = [] | ||||||
| unstable-msc3488 = ["unstable-msc1767"] | unstable-msc3488 = ["unstable-msc1767"] | ||||||
|  | unstable-msc3489 = ["unstable-msc3488"] | ||||||
| unstable-msc3551 = ["unstable-msc3956"] | unstable-msc3551 = ["unstable-msc3956"] | ||||||
| unstable-msc3552 = ["unstable-msc3551"] | unstable-msc3552 = ["unstable-msc3551"] | ||||||
| unstable-msc3553 = ["unstable-msc3552"] | unstable-msc3553 = ["unstable-msc3552"] | ||||||
| @ -41,7 +41,7 @@ unstable-msc3927 = ["unstable-msc3551"] | |||||||
| unstable-msc3954 = ["unstable-msc1767"] | unstable-msc3954 = ["unstable-msc1767"] | ||||||
| unstable-msc3955 = ["unstable-msc1767"] | unstable-msc3955 = ["unstable-msc1767"] | ||||||
| unstable-msc3956 = ["unstable-msc1767"] | unstable-msc3956 = ["unstable-msc1767"] | ||||||
| unstable-msc4075 = [] | unstable-msc4075 = ["unstable-msc3401"] | ||||||
| unstable-pdu = [] | unstable-pdu = [] | ||||||
| 
 | 
 | ||||||
| # Allow some mandatory fields to be missing, defaulting them to an empty string | # Allow some mandatory fields to be missing, defaulting them to an empty string | ||||||
| @ -65,7 +65,7 @@ indexmap = { version = "2.0.0", features = ["serde"] } | |||||||
| js_int = { workspace = true, features = ["serde"] } | js_int = { workspace = true, features = ["serde"] } | ||||||
| js_option = "0.1.0" | js_option = "0.1.0" | ||||||
| percent-encoding = "2.1.0" | percent-encoding = "2.1.0" | ||||||
| pulldown-cmark = { version = "0.10.3", optional = true, default-features = false, features = ["html"] } | pulldown-cmark = { version = "0.11.0", optional = true, default-features = false, features = ["html"] } | ||||||
| regex = { version = "1.5.6", default-features = false, features = ["std", "perf"] } | regex = { version = "1.5.6", default-features = false, features = ["std", "perf"] } | ||||||
| ruma-common = { workspace = true } | ruma-common = { workspace = true } | ||||||
| ruma-html = { workspace = true, optional = true } | ruma-html = { workspace = true, optional = true } | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								crates/ruma-events/src/beacon.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								crates/ruma-events/src/beacon.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | //! Types for the `org.matrix.msc3489.beacon` event, the unstable version of
 | ||||||
|  | //! `m.beacon` ([MSC3489]).
 | ||||||
|  | //!
 | ||||||
|  | //! [MSC3489]: https://github.com/matrix-org/matrix-spec-proposals/pull/3489
 | ||||||
|  | 
 | ||||||
|  | use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedEventId}; | ||||||
|  | use ruma_events::{location::LocationContent, relation::Reference}; | ||||||
|  | use ruma_macros::EventContent; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | 
 | ||||||
|  | /// The content of a beacon.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[ruma_event(type = "org.matrix.msc3672.beacon", alias = "m.beacon", kind = MessageLike)] | ||||||
|  | pub struct BeaconEventContent { | ||||||
|  |     /// The beacon_info event id this relates to.
 | ||||||
|  |     #[serde(rename = "m.relates_to")] | ||||||
|  |     pub relates_to: Reference, | ||||||
|  | 
 | ||||||
|  |     /// The location of the beacon.
 | ||||||
|  |     #[serde(rename = "org.matrix.msc3488.location")] | ||||||
|  |     pub location: LocationContent, | ||||||
|  | 
 | ||||||
|  |     /// The timestamp of the event.
 | ||||||
|  |     #[serde(rename = "org.matrix.msc3488.ts")] | ||||||
|  |     pub ts: MilliSecondsSinceUnixEpoch, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl BeaconEventContent { | ||||||
|  |     /// Creates a new `BeaconEventContent` with the given beacon_info event id, geo uri and
 | ||||||
|  |     /// optional ts. If ts is None, the current time will be used.
 | ||||||
|  |     pub fn new( | ||||||
|  |         beacon_info_event_id: OwnedEventId, | ||||||
|  |         geo_uri: String, | ||||||
|  |         ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             relates_to: Reference::new(beacon_info_event_id), | ||||||
|  |             location: LocationContent::new(geo_uri), | ||||||
|  |             ts: ts.unwrap_or_else(MilliSecondsSinceUnixEpoch::now), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										83
									
								
								crates/ruma-events/src/beacon_info.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								crates/ruma-events/src/beacon_info.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | |||||||
|  | //! Types for the `org.matrix.msc3489.beacon_info` state event, the unstable version of
 | ||||||
|  | //! `m.beacon_info` ([MSC3489]).
 | ||||||
|  | //!
 | ||||||
|  | //! [MSC3489]: https://github.com/matrix-org/matrix-spec-proposals/pull/3489
 | ||||||
|  | 
 | ||||||
|  | use std::time::{Duration, SystemTime}; | ||||||
|  | 
 | ||||||
|  | use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedUserId}; | ||||||
|  | use ruma_macros::EventContent; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | 
 | ||||||
|  | use crate::location::AssetContent; | ||||||
|  | 
 | ||||||
|  | /// The content of a beacon_info state.
 | ||||||
|  | #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[ruma_event(
 | ||||||
|  |     type = "org.matrix.msc3672.beacon_info", alias = "m.beacon_info", kind = State, state_key_type = OwnedUserId | ||||||
|  | )] | ||||||
|  | pub struct BeaconInfoEventContent { | ||||||
|  |     /// The description of the location.
 | ||||||
|  |     ///
 | ||||||
|  |     /// It should be used to label the location on a map.
 | ||||||
|  |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub description: Option<String>, | ||||||
|  | 
 | ||||||
|  |     /// Whether the user starts sharing their location.
 | ||||||
|  |     pub live: bool, | ||||||
|  | 
 | ||||||
|  |     /// The time when location sharing started.
 | ||||||
|  |     #[serde(rename = "org.matrix.msc3488.ts")] | ||||||
|  |     pub ts: MilliSecondsSinceUnixEpoch, | ||||||
|  | 
 | ||||||
|  |     /// The duration that the location sharing will be live.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Meaning that the location will stop being shared at `ts + timeout`.
 | ||||||
|  |     #[serde(default, with = "ruma_common::serde::duration::ms")] | ||||||
|  |     pub timeout: Duration, | ||||||
|  | 
 | ||||||
|  |     /// The asset that this message refers to.
 | ||||||
|  |     #[serde(default, rename = "org.matrix.msc3488.asset")] | ||||||
|  |     pub asset: AssetContent, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl BeaconInfoEventContent { | ||||||
|  |     /// Creates a new `BeaconInfoEventContent` with the given description, live, timeout and
 | ||||||
|  |     /// optional ts. If ts is None, the current time will be used.
 | ||||||
|  |     pub fn new( | ||||||
|  |         description: Option<String>, | ||||||
|  |         timeout: Duration, | ||||||
|  |         live: bool, | ||||||
|  |         ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             description, | ||||||
|  |             live, | ||||||
|  |             ts: ts.unwrap_or_else(MilliSecondsSinceUnixEpoch::now), | ||||||
|  |             timeout, | ||||||
|  |             asset: Default::default(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Starts the beacon_info being live.
 | ||||||
|  |     pub fn start(&mut self) { | ||||||
|  |         self.live = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Stops the beacon_info from being live.
 | ||||||
|  |     pub fn stop(&mut self) { | ||||||
|  |         self.live = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Start time plus its timeout, it returns `false`, indicating that the beacon is not live.
 | ||||||
|  |     /// Otherwise, it returns `true`.
 | ||||||
|  |     pub fn is_live(&self) -> bool { | ||||||
|  |         self.live | ||||||
|  |             && self | ||||||
|  |                 .ts | ||||||
|  |                 .to_system_time() | ||||||
|  |                 .and_then(|t| t.checked_add(self.timeout)) | ||||||
|  |                 .is_some_and(|t| t > SystemTime::now()) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -12,7 +12,6 @@ pub mod negotiate; | |||||||
| #[cfg(feature = "unstable-msc4075")] | #[cfg(feature = "unstable-msc4075")] | ||||||
| pub mod notify; | pub mod notify; | ||||||
| pub mod reject; | pub mod reject; | ||||||
| #[cfg(feature = "unstable-msc3291")] |  | ||||||
| pub mod sdp_stream_metadata_changed; | pub mod sdp_stream_metadata_changed; | ||||||
| pub mod select_answer; | pub mod select_answer; | ||||||
| 
 | 
 | ||||||
| @ -59,14 +58,12 @@ pub struct StreamMetadata { | |||||||
|     /// Whether the audio track of the stream is muted.
 |     /// Whether the audio track of the stream is muted.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// Defaults to `false`.
 |     /// Defaults to `false`.
 | ||||||
|     #[cfg(feature = "unstable-msc3291")] |  | ||||||
|     #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] |     #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] | ||||||
|     pub audio_muted: bool, |     pub audio_muted: bool, | ||||||
| 
 | 
 | ||||||
|     /// Whether the video track of the stream is muted.
 |     /// Whether the video track of the stream is muted.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// Defaults to `false`.
 |     /// Defaults to `false`.
 | ||||||
|     #[cfg(feature = "unstable-msc3291")] |  | ||||||
|     #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] |     #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] | ||||||
|     pub video_muted: bool, |     pub video_muted: bool, | ||||||
| } | } | ||||||
| @ -74,13 +71,7 @@ pub struct StreamMetadata { | |||||||
| impl StreamMetadata { | impl StreamMetadata { | ||||||
|     /// Creates a new `StreamMetadata` with the given purpose.
 |     /// Creates a new `StreamMetadata` with the given purpose.
 | ||||||
|     pub fn new(purpose: StreamPurpose) -> Self { |     pub fn new(purpose: StreamPurpose) -> Self { | ||||||
|         Self { |         Self { purpose, audio_muted: false, video_muted: false } | ||||||
|             purpose, |  | ||||||
|             #[cfg(feature = "unstable-msc3291")] |  | ||||||
|             audio_muted: false, |  | ||||||
|             #[cfg(feature = "unstable-msc3291")] |  | ||||||
|             video_muted: false, |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,65 +1,118 @@ | |||||||
| //! Types for matrixRTC state events ([MSC3401]).
 | //! Types for MatrixRTC state events ([MSC3401]).
 | ||||||
| //!
 | //!
 | ||||||
| //! This implements a newer/updated version of MSC3401.
 | //! This implements a newer/updated version of MSC3401.
 | ||||||
| //!
 | //!
 | ||||||
| //! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
 | //! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
 | ||||||
| 
 | 
 | ||||||
| use std::time::Duration; | mod focus; | ||||||
|  | mod member_data; | ||||||
| 
 | 
 | ||||||
| use as_variant::as_variant; | pub use focus::*; | ||||||
| use ruma_common::{serde::StringEnum, MilliSecondsSinceUnixEpoch, OwnedUserId}; | pub use member_data::*; | ||||||
| use ruma_macros::EventContent; | use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedUserId}; | ||||||
|  | use ruma_macros::{EventContent, StringEnum}; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use tracing::warn; |  | ||||||
| 
 | 
 | ||||||
| use crate::PrivOwnedStr; | use crate::{ | ||||||
|  |     PossiblyRedactedStateEventContent, PrivOwnedStr, RedactContent, RedactedStateEventContent, | ||||||
|  |     StateEventType, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| /// The member state event for a matrixRTC session.
 | /// The member state event for a MatrixRTC session.
 | ||||||
| ///
 | ///
 | ||||||
| /// This is the object containing all the data related to a matrix users participation in a
 | /// This is the object containing all the data related to a Matrix users participation in a
 | ||||||
| /// matrixRTC session. It consists of memberships / sessions.
 | /// MatrixRTC session.
 | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] | ///
 | ||||||
| #[cfg_attr(test, derive(PartialEq))] | /// This is a unit struct with the enum [`CallMemberEventContent`] because a Ruma state event cannot
 | ||||||
|  | /// be an enum and we need this to be an untagged enum for parsing purposes. (see
 | ||||||
|  | /// [`CallMemberEventContent`])
 | ||||||
|  | ///
 | ||||||
|  | /// This struct also exposes allows to call the methods from [`CallMemberEventContent`].
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)] | ||||||
|  | #[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = OwnedUserId, custom_redacted, custom_possibly_redacted)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| #[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = OwnedUserId)] | #[serde(untagged)] | ||||||
| pub struct CallMemberEventContent { | pub enum CallMemberEventContent { | ||||||
|     /// A list of all the memberships that user currently has in this room.
 |     /// The legacy format for m.call.member events. (An array of memberships. The devices of one
 | ||||||
|     ///
 |     /// user.)
 | ||||||
|     /// There can be multiple ones in cases the user participates with multiple devices or there
 |     LegacyContent(LegacyMembershipContent), | ||||||
|     /// are multiple RTC applications running.
 |     /// Normal membership events. One event per membership. Multiple state keys will
 | ||||||
|     ///
 |     /// be used to describe multiple devices for one user.
 | ||||||
|     /// e.g. a call and a spacial experience.
 |     SessionContent(SessionMembershipData), | ||||||
|     ///
 |     /// An empty content means this user has been in a rtc session but is not anymore.
 | ||||||
|     /// Important: This includes expired memberships.
 |     Empty(EmptyMembershipData), | ||||||
|     /// To retrieve a list including only valid memberships,
 |  | ||||||
|     /// see [`active_memberships`](CallMemberEventContent::active_memberships).
 |  | ||||||
|     pub memberships: Vec<Membership>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl CallMemberEventContent { | impl CallMemberEventContent { | ||||||
|     /// Creates a new `CallMemberEventContent`.
 |     /// Creates a new [`CallMemberEventContent`] with [`LegacyMembershipData`].
 | ||||||
|     pub fn new(memberships: Vec<Membership>) -> Self { |     pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self { | ||||||
|         Self { memberships } |         Self::LegacyContent(LegacyMembershipContent { | ||||||
|  |             memberships, //: memberships.into_iter().map(MembershipData::Legacy).collect(),
 | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Creates a new [`CallMemberEventContent`] with [`SessionMembershipData`].
 | ||||||
|  |     pub fn new( | ||||||
|  |         application: Application, | ||||||
|  |         device_id: String, | ||||||
|  |         focus_active: ActiveFocus, | ||||||
|  |         foci_preferred: Vec<Focus>, | ||||||
|  |         created_ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self::SessionContent(SessionMembershipData { | ||||||
|  |             application, | ||||||
|  |             device_id, | ||||||
|  |             focus_active, | ||||||
|  |             foci_preferred, | ||||||
|  |             created_ts, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Creates a new Empty [`CallMemberEventContent`] representing a left membership.
 | ||||||
|  |     pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self { | ||||||
|  |         Self::Empty(EmptyMembershipData { leave_reason }) | ||||||
|  |     } | ||||||
|     /// All non expired memberships in this member event.
 |     /// All non expired memberships in this member event.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// In most cases you want tu use this method instead of the public memberships field.
 |     /// In most cases you want to use this method instead of the public memberships field.
 | ||||||
|     /// The memberships field will also include expired events.
 |     /// The memberships field will also include expired events.
 | ||||||
|     ///
 |     ///
 | ||||||
|  |     /// This copies all the memberships and converts them
 | ||||||
|     /// # Arguments
 |     /// # Arguments
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in case
 |     /// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in the
 | ||||||
|     ///   the Membership does not contain `created_ts`. (`origin_server_ts` will be ignored if
 |     ///   Membership does not contain [`LegacyMembershipData::created_ts`]. (`origin_server_ts` will
 | ||||||
|     ///   `created_ts` is `Some`)
 |     ///   be ignored if [`LegacyMembershipData::created_ts`] is `Some`)
 | ||||||
|     pub fn active_memberships( |     pub fn active_memberships( | ||||||
|         &self, |         &self, | ||||||
|         origin_server_ts: Option<MilliSecondsSinceUnixEpoch>, |         origin_server_ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|     ) -> Vec<&Membership> { |     ) -> Vec<MembershipData<'_>> { | ||||||
|         self.memberships.iter().filter(|m| !m.is_expired(origin_server_ts)).collect() |         match self { | ||||||
|  |             CallMemberEventContent::LegacyContent(content) => { | ||||||
|  |                 content.active_memberships(origin_server_ts) | ||||||
|  |             } | ||||||
|  |             CallMemberEventContent::SessionContent(content) => { | ||||||
|  |                 [content].map(MembershipData::Session).to_vec() | ||||||
|  |             } | ||||||
|  |             CallMemberEventContent::Empty(_) => Vec::new(), | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the `created_ts` of each [Membership] in this event.
 |     /// All the memberships for this event. Can only contain multiple elements in the case of legacy
 | ||||||
|  |     /// `m.call.member` state events.
 | ||||||
|  |     pub fn memberships(&self) -> Vec<MembershipData<'_>> { | ||||||
|  |         match self { | ||||||
|  |             CallMemberEventContent::LegacyContent(content) => { | ||||||
|  |                 content.memberships.iter().map(MembershipData::Legacy).collect() | ||||||
|  |             } | ||||||
|  |             CallMemberEventContent::SessionContent(content) => { | ||||||
|  |                 [content].map(MembershipData::Session).to_vec() | ||||||
|  |             } | ||||||
|  |             CallMemberEventContent::Empty(_) => Vec::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Set the `created_ts` of each [`MembershipData::Legacy`] in this event.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// Each call member event contains the `origin_server_ts` and `content.create_ts`.
 |     /// Each call member event contains the `origin_server_ts` and `content.create_ts`.
 | ||||||
|     /// `content.create_ts` is undefined for the initial event of a session (because the
 |     /// `content.create_ts` is undefined for the initial event of a session (because the
 | ||||||
| @ -68,257 +121,130 @@ impl CallMemberEventContent { | |||||||
|     /// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
 |     /// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
 | ||||||
|     /// expired)
 |     /// expired)
 | ||||||
|     pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) { |     pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) { | ||||||
|         self.memberships.iter_mut().for_each(|m| { |         match self { | ||||||
|             m.created_ts.get_or_insert(origin_server_ts); |             CallMemberEventContent::LegacyContent(content) => { | ||||||
|         }); |                 content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| { | ||||||
|     } |                     m.created_ts.get_or_insert(origin_server_ts); | ||||||
| } |                 }); | ||||||
| 
 |             } | ||||||
| /// A membership describes one of the sessions this user currently partakes.
 |             CallMemberEventContent::SessionContent(m) => { | ||||||
| ///
 |                 m.created_ts.get_or_insert(origin_server_ts); | ||||||
| /// The application defines the type of the session.
 |             } | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] |             _ => (), | ||||||
| #[cfg_attr(test, derive(PartialEq))] |  | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] |  | ||||||
| pub struct Membership { |  | ||||||
|     /// The type of the matrixRTC session the membership belongs to.
 |  | ||||||
|     ///
 |  | ||||||
|     /// e.g. call, spacial, document...
 |  | ||||||
|     #[serde(flatten)] |  | ||||||
|     pub application: Application, |  | ||||||
| 
 |  | ||||||
|     /// The device id of this membership.
 |  | ||||||
|     ///
 |  | ||||||
|     /// The same user can join with their phone/computer.
 |  | ||||||
|     pub device_id: String, |  | ||||||
| 
 |  | ||||||
|     /// The duration in milliseconds relative to the time this membership joined
 |  | ||||||
|     /// during which the membership is valid.
 |  | ||||||
|     ///
 |  | ||||||
|     /// The time a member has joined is defined as:
 |  | ||||||
|     /// `MIN(content.created_ts, event.origin_server_ts)`
 |  | ||||||
|     #[serde(with = "ruma_common::serde::duration::ms")] |  | ||||||
|     pub expires: Duration, |  | ||||||
| 
 |  | ||||||
|     /// Stores a copy of the `origin_server_ts` of the initial session event.
 |  | ||||||
|     ///
 |  | ||||||
|     /// If the membership is updated this field will be used to track to
 |  | ||||||
|     /// original `origin_server_ts`.
 |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub created_ts: Option<MilliSecondsSinceUnixEpoch>, |  | ||||||
| 
 |  | ||||||
|     /// A list of the foci in use for this membership.
 |  | ||||||
|     pub foci_active: Vec<Focus>, |  | ||||||
| 
 |  | ||||||
|     /// The id of the membership.
 |  | ||||||
|     ///
 |  | ||||||
|     /// This is required to guarantee uniqueness of the event.
 |  | ||||||
|     /// Sending the same state event twice to synapse makes the HS drop the second one and return
 |  | ||||||
|     /// 200.
 |  | ||||||
|     #[serde(rename = "membershipID")] |  | ||||||
|     pub membership_id: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Membership { |  | ||||||
|     /// The application of the membership is "m.call" and the scope is "m.room".
 |  | ||||||
|     pub fn is_room_call(&self) -> bool { |  | ||||||
|         as_variant!(&self.application, Application::Call) |  | ||||||
|             .is_some_and(|call| call.scope == CallScope::Room) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// The application of the membership is "m.call".
 |  | ||||||
|     pub fn is_call(&self) -> bool { |  | ||||||
|         as_variant!(&self.application, Application::Call).is_some() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Checks if the event is expired.
 |  | ||||||
|     ///
 |  | ||||||
|     /// Defaults to using `created_ts` of the `Membership`.
 |  | ||||||
|     /// If no `origin_server_ts` is provided and the event does not contain `created_ts`
 |  | ||||||
|     /// the event will be considered as not expired.
 |  | ||||||
|     /// In this case, a warning will be logged.
 |  | ||||||
|     ///
 |  | ||||||
|     /// # Arguments
 |  | ||||||
|     ///
 |  | ||||||
|     /// * `origin_server_ts` - a fallback if `created_ts` is not present
 |  | ||||||
|     pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool { |  | ||||||
|         let ev_created_ts = self.created_ts.or(origin_server_ts); |  | ||||||
| 
 |  | ||||||
|         if let Some(ev_created_ts) = ev_created_ts { |  | ||||||
|             let now = MilliSecondsSinceUnixEpoch::now().to_system_time(); |  | ||||||
|             let expire_ts = ev_created_ts.to_system_time().map(|t| t + self.expires); |  | ||||||
|             now > expire_ts |  | ||||||
|         } else { |  | ||||||
|             // This should not be reached since we only allow events that have copied over
 |  | ||||||
|             // the origin server ts. `set_created_ts_if_none`
 |  | ||||||
|             warn!("Encountered a Call Member state event where the origin_ts (or origin_server_ts) could not be found.\ |  | ||||||
|             It is treated as a non expired event but this might be wrong.");
 |  | ||||||
|             false |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Initial set of fields of [`Membership`].
 | /// This describes the CallMember event if the user is not part of the current session.
 | ||||||
| #[derive(Debug)] | #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] | ||||||
| #[allow(clippy::exhaustive_structs)] |  | ||||||
| pub struct MembershipInit { |  | ||||||
|     /// The type of the matrixRTC session the membership belongs to.
 |  | ||||||
|     ///
 |  | ||||||
|     /// e.g. call, spacial, document...
 |  | ||||||
|     pub application: Application, |  | ||||||
| 
 |  | ||||||
|     /// The device id of this membership.
 |  | ||||||
|     ///
 |  | ||||||
|     /// The same user can join with their phone/computer.
 |  | ||||||
|     pub device_id: String, |  | ||||||
| 
 |  | ||||||
|     /// The duration in milliseconds relative to the time this membership joined
 |  | ||||||
|     /// during which the membership is valid.
 |  | ||||||
|     ///
 |  | ||||||
|     /// The time a member has joined is defined as:
 |  | ||||||
|     /// `MIN(content.created_ts, event.origin_server_ts)`
 |  | ||||||
|     pub expires: Duration, |  | ||||||
| 
 |  | ||||||
|     /// A list of the focuses (foci) in use for this membership.
 |  | ||||||
|     pub foci_active: Vec<Focus>, |  | ||||||
| 
 |  | ||||||
|     /// The id of the membership.
 |  | ||||||
|     ///
 |  | ||||||
|     /// This is required to guarantee uniqueness of the event.
 |  | ||||||
|     /// Sending the same state event twice to synapse makes the HS drop the second one and return
 |  | ||||||
|     /// 200.
 |  | ||||||
|     pub membership_id: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<MembershipInit> for Membership { |  | ||||||
|     fn from(init: MembershipInit) -> Self { |  | ||||||
|         let MembershipInit { application, device_id, expires, foci_active, membership_id } = init; |  | ||||||
|         Self { application, device_id, expires, created_ts: None, foci_active, membership_id } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Description of the SFU/Focus a membership can be connected to.
 |  | ||||||
| ///
 |  | ||||||
| /// A focus can be any server powering the matrixRTC session (SFU,
 |  | ||||||
| /// MCU). It serves as a node to redistribute RTC streams.
 |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] |  | ||||||
| #[cfg_attr(test, derive(PartialEq))] |  | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| #[serde(tag = "type", rename_all = "snake_case")] | pub struct EmptyMembershipData { | ||||||
| pub enum Focus { |     /// An empty call member state event can optionally contain a leave reason.
 | ||||||
|     /// Livekit is one possible type of SFU/Focus that can be used for a matrixRTC session.
 |     /// If it is `None` the user has left the call ordinarily. (Intentional hangup)  
 | ||||||
|     Livekit(LivekitFocus), |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub leave_reason: Option<LeaveReason>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// The fields to describe livekit as an `active_foci`.
 | /// This is the optional value for an empty membership event content:
 | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | /// [`CallMemberEventContent::Empty`]. It is used when the user disconnected and a Future ([MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140))
 | ||||||
| #[cfg_attr(test, derive(PartialEq))] | /// was used to update the membership after the client was not reachable anymore.  
 | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] |  | ||||||
| pub struct LivekitFocus { |  | ||||||
|     /// The alias where the livekit sessions can be reached.
 |  | ||||||
|     #[serde(rename = "livekit_alias")] |  | ||||||
|     pub alias: String, |  | ||||||
| 
 |  | ||||||
|     /// The url of the jwt server for the livekit instance.
 |  | ||||||
|     #[serde(rename = "livekit_service_url")] |  | ||||||
|     pub service_url: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl LivekitFocus { |  | ||||||
|     /// Initialize a [`LivekitFocus`].
 |  | ||||||
|     ///
 |  | ||||||
|     /// # Arguments
 |  | ||||||
|     ///
 |  | ||||||
|     /// * `alias` - The alias where the livekit sessions can be reached.
 |  | ||||||
|     /// * `service_url` - The url of the jwt server for the livekit instance.
 |  | ||||||
|     pub fn new(alias: String, service_url: String) -> Self { |  | ||||||
|         Self { alias, service_url } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// The type of the matrixRTC session.
 |  | ||||||
| ///
 |  | ||||||
| /// This is not the application/client used by the user but the
 |  | ||||||
| /// type of matrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
 |  | ||||||
| /// possible applications.
 |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] |  | ||||||
| #[cfg_attr(test, derive(PartialEq))] |  | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] |  | ||||||
| #[serde(tag = "application")] |  | ||||||
| pub enum Application { |  | ||||||
|     #[serde(rename = "m.call")] |  | ||||||
|     /// A VoIP call.
 |  | ||||||
|     Call(CallApplicationContent), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Call specific parameters membership parameters.
 |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] |  | ||||||
| #[cfg_attr(test, derive(PartialEq))] |  | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] |  | ||||||
| pub struct CallApplicationContent { |  | ||||||
|     /// An identifier for calls.
 |  | ||||||
|     ///
 |  | ||||||
|     /// All members using the same `call_id` will end up in the same call.
 |  | ||||||
|     ///
 |  | ||||||
|     /// Does not need to be a uuid.
 |  | ||||||
|     ///
 |  | ||||||
|     /// `""` is used for room scoped calls.
 |  | ||||||
|     pub call_id: String, |  | ||||||
| 
 |  | ||||||
|     /// Who owns/joins/controls (can modify) the call.
 |  | ||||||
|     pub scope: CallScope, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl CallApplicationContent { |  | ||||||
|     /// Initialize a [`CallApplicationContent`].
 |  | ||||||
|     ///
 |  | ||||||
|     /// # Arguments
 |  | ||||||
|     ///
 |  | ||||||
|     /// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
 |  | ||||||
|     ///   the same call. Does not need to be a uuid. `""` is used for room scoped calls.
 |  | ||||||
|     /// * `scope` - Who owns/joins/controls (can modify) the call.
 |  | ||||||
|     pub fn new(call_id: String, scope: CallScope) -> Self { |  | ||||||
|         Self { call_id, scope } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// The call scope defines different call ownership models.
 |  | ||||||
| #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] |  | ||||||
| #[derive(Clone, PartialEq, StringEnum)] | #[derive(Clone, PartialEq, StringEnum)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| #[ruma_enum(rename_all = "m.snake_case")] | #[ruma_enum(rename_all = "m.snake_case")] | ||||||
| pub enum CallScope { | pub enum LeaveReason { | ||||||
|     /// A call which every user of a room can join and create.
 |     /// The user left the call by losing network connection or closing  
 | ||||||
|     ///
 |     /// the client before it was able to send the leave event.
 | ||||||
|     /// There is no particular name associated with it.
 |     LostConnection, | ||||||
|     ///
 |  | ||||||
|     /// There can only be one per room.
 |  | ||||||
|     Room, |  | ||||||
| 
 |  | ||||||
|     /// A user call is owned by a user.
 |  | ||||||
|     ///
 |  | ||||||
|     /// Each user can create one there can be multiple per room. They are started and ended by the
 |  | ||||||
|     /// owning user.
 |  | ||||||
|     User, |  | ||||||
| 
 |  | ||||||
|     #[doc(hidden)] |     #[doc(hidden)] | ||||||
|     _Custom(PrivOwnedStr), |     _Custom(PrivOwnedStr), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl RedactContent for CallMemberEventContent { | ||||||
|  |     type Redacted = RedactedCallMemberEventContent; | ||||||
|  | 
 | ||||||
|  |     fn redact(self, _version: &ruma_common::RoomVersionId) -> Self::Redacted { | ||||||
|  |         RedactedCallMemberEventContent {} | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The PossiblyRedacted version of [`CallMemberEventContent`].
 | ||||||
|  | ///
 | ||||||
|  | /// Since [`CallMemberEventContent`] has the [`CallMemberEventContent::Empty`] state it already is
 | ||||||
|  | /// compatible with the redacted version of the state event content.
 | ||||||
|  | pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent; | ||||||
|  | 
 | ||||||
|  | impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent { | ||||||
|  |     type StateKey = OwnedUserId; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The Redacted version of [`CallMemberEventContent`].
 | ||||||
|  | #[derive(Clone, Debug, Deserialize, Serialize)] | ||||||
|  | #[allow(clippy::exhaustive_structs)] | ||||||
|  | pub struct RedactedCallMemberEventContent {} | ||||||
|  | 
 | ||||||
|  | impl ruma_events::content::EventContent for RedactedCallMemberEventContent { | ||||||
|  |     type EventType = StateEventType; | ||||||
|  |     fn event_type(&self) -> Self::EventType { | ||||||
|  |         StateEventType::CallMember | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl RedactedStateEventContent for RedactedCallMemberEventContent { | ||||||
|  |     type StateKey = OwnedUserId; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub struct LegacyMembershipContent { | ||||||
|  |     /// A list of all the memberships that user currently has in this room.
 | ||||||
|  |     ///
 | ||||||
|  |     /// There can be multiple ones in case the user participates with multiple devices or there
 | ||||||
|  |     /// are multiple RTC applications running.
 | ||||||
|  |     ///
 | ||||||
|  |     /// e.g. a call and a spacial experience.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Important: This includes expired memberships.
 | ||||||
|  |     /// To retrieve a list including only valid memberships,
 | ||||||
|  |     /// see [`active_memberships`](CallMemberEventContent::active_memberships).
 | ||||||
|  |     memberships: Vec<LegacyMembershipData>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl LegacyMembershipContent { | ||||||
|  |     fn active_memberships( | ||||||
|  |         &self, | ||||||
|  |         origin_server_ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  |     ) -> Vec<MembershipData<'_>> { | ||||||
|  |         self.memberships | ||||||
|  |             .iter() | ||||||
|  |             .filter(|m| !m.is_expired(origin_server_ts)) | ||||||
|  |             .map(MembershipData::Legacy) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use std::time::Duration; |     use std::time::Duration; | ||||||
| 
 | 
 | ||||||
|     use ruma_common::MilliSecondsSinceUnixEpoch as TS; |     use assert_matches2::assert_matches; | ||||||
|     use serde_json::json; |     use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId}; | ||||||
|  |     use serde_json::{from_value as from_json_value, json}; | ||||||
| 
 | 
 | ||||||
|     use super::{ |     use super::{ | ||||||
|         Application, CallApplicationContent, CallMemberEventContent, CallScope, Focus, |         focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus}, | ||||||
|         LivekitFocus, Membership, |         member_data::{ | ||||||
|  |             Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData, | ||||||
|  |         }, | ||||||
|  |         CallMemberEventContent, | ||||||
|  |     }; | ||||||
|  |     use crate::{ | ||||||
|  |         call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData}, | ||||||
|  |         AnyStateEvent, StateEvent, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     fn create_call_member_event_content() -> CallMemberEventContent { |     fn create_call_member_legacy_event_content() -> CallMemberEventContent { | ||||||
|         CallMemberEventContent::new(vec![Membership { |         CallMemberEventContent::new_legacy(vec![LegacyMembershipData { | ||||||
|             application: Application::Call(CallApplicationContent { |             application: Application::Call(CallApplicationContent { | ||||||
|                 call_id: "123456".to_owned(), |                 call_id: "123456".to_owned(), | ||||||
|                 scope: CallScope::Room, |                 scope: CallScope::Room, | ||||||
| @ -334,8 +260,60 @@ mod tests { | |||||||
|         }]) |         }]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fn create_call_member_event_content() -> CallMemberEventContent { | ||||||
|  |         CallMemberEventContent::new( | ||||||
|  |             Application::Call(CallApplicationContent { | ||||||
|  |                 call_id: "123456".to_owned(), | ||||||
|  |                 scope: CallScope::Room, | ||||||
|  |             }), | ||||||
|  |             "ABCDE".to_owned(), | ||||||
|  |             ActiveFocus::Livekit(ActiveLivekitFocus { | ||||||
|  |                 focus_select: FocusSelection::OldestMembership, | ||||||
|  |             }), | ||||||
|  |             vec![Focus::Livekit(LivekitFocus { | ||||||
|  |                 alias: "1".to_owned(), | ||||||
|  |                 service_url: "https://livekit.com".to_owned(), | ||||||
|  |             })], | ||||||
|  |             None, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn serialize_call_member_event_content() { |     fn serialize_call_member_event_content() { | ||||||
|  |         let call_member_event = &json!({ | ||||||
|  |             "application": "m.call", | ||||||
|  |             "call_id": "123456", | ||||||
|  |             "scope": "m.room", | ||||||
|  |             "device_id": "ABCDE", | ||||||
|  |             "foci_preferred": [ | ||||||
|  |                 { | ||||||
|  |                     "livekit_alias": "1", | ||||||
|  |                     "livekit_service_url": "https://livekit.com", | ||||||
|  |                     "type": "livekit" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "focus_active":{ | ||||||
|  |                 "type":"livekit", | ||||||
|  |                 "focus_select":"oldest_membership" | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         assert_eq!( | ||||||
|  |             call_member_event, | ||||||
|  |             &serde_json::to_value(create_call_member_event_content()).unwrap() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let empty_call_member_event = &json!({}); | ||||||
|  |         assert_eq!( | ||||||
|  |             empty_call_member_event, | ||||||
|  |             &serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData { | ||||||
|  |                 leave_reason: None | ||||||
|  |             })) | ||||||
|  |             .unwrap() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn serialize_legacy_call_member_event_content() { | ||||||
|         let call_member_event = &json!({ |         let call_member_event = &json!({ | ||||||
|             "memberships": [ |             "memberships": [ | ||||||
|                 { |                 { | ||||||
| @ -358,14 +336,62 @@ mod tests { | |||||||
| 
 | 
 | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             call_member_event, |             call_member_event, | ||||||
|             &serde_json::to_value(create_call_member_event_content()).unwrap() |             &serde_json::to_value(create_call_member_legacy_event_content()).unwrap() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     #[test] | ||||||
|  |     fn deserialize_call_member_event_content() { | ||||||
|  |         let call_member_ev = CallMemberEventContent::new( | ||||||
|  |             Application::Call(CallApplicationContent { | ||||||
|  |                 call_id: "123456".to_owned(), | ||||||
|  |                 scope: CallScope::Room, | ||||||
|  |             }), | ||||||
|  |             "THIS_DEVICE".to_owned(), | ||||||
|  |             ActiveFocus::Livekit(ActiveLivekitFocus { | ||||||
|  |                 focus_select: FocusSelection::OldestMembership, | ||||||
|  |             }), | ||||||
|  |             vec![Focus::Livekit(LivekitFocus { | ||||||
|  |                 alias: "room1".to_owned(), | ||||||
|  |                 service_url: "https://livekit1.com".to_owned(), | ||||||
|  |             })], | ||||||
|  |             None, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let call_member_ev_json = json!({ | ||||||
|  |             "application": "m.call", | ||||||
|  |             "call_id": "123456", | ||||||
|  |             "scope": "m.room", | ||||||
|  |             "device_id": "THIS_DEVICE", | ||||||
|  |             "focus_active":{ | ||||||
|  |                 "type": "livekit", | ||||||
|  |                 "focus_select": "oldest_membership" | ||||||
|  |             }, | ||||||
|  |             "foci_preferred": [ | ||||||
|  |                 { | ||||||
|  |                     "livekit_alias": "room1", | ||||||
|  |                     "livekit_service_url": "https://livekit1.com", | ||||||
|  |                     "type": "livekit" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         let ev_content: CallMemberEventContent = | ||||||
|  |             serde_json::from_value(call_member_ev_json).unwrap(); | ||||||
|  |         assert_eq!( | ||||||
|  |             serde_json::to_string(&ev_content).unwrap(), | ||||||
|  |             serde_json::to_string(&call_member_ev).unwrap() | ||||||
|  |         ); | ||||||
|  |         let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }); | ||||||
|  |         assert_eq!( | ||||||
|  |             serde_json::to_string(&json!({})).unwrap(), | ||||||
|  |             serde_json::to_string(&empty).unwrap() | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn deserialize_call_member_event_content() { |     fn deserialize_legacy_call_member_event_content() { | ||||||
|         let call_member_ev: CallMemberEventContent = CallMemberEventContent::new(vec![ |         let call_member_ev = CallMemberEventContent::new_legacy(vec![ | ||||||
|             Membership { |             LegacyMembershipData { | ||||||
|                 application: Application::Call(CallApplicationContent { |                 application: Application::Call(CallApplicationContent { | ||||||
|                     call_id: "123456".to_owned(), |                     call_id: "123456".to_owned(), | ||||||
|                     scope: CallScope::Room, |                     scope: CallScope::Room, | ||||||
| @ -379,7 +405,7 @@ mod tests { | |||||||
|                 membership_id: "0".to_owned(), |                 membership_id: "0".to_owned(), | ||||||
|                 created_ts: None, |                 created_ts: None, | ||||||
|             }, |             }, | ||||||
|             Membership { |             LegacyMembershipData { | ||||||
|                 application: Application::Call(CallApplicationContent { |                 application: Application::Call(CallApplicationContent { | ||||||
|                     call_id: "".to_owned(), |                     call_id: "".to_owned(), | ||||||
|                     scope: CallScope::Room, |                     scope: CallScope::Room, | ||||||
| @ -432,7 +458,85 @@ mod tests { | |||||||
| 
 | 
 | ||||||
|         let ev_content: CallMemberEventContent = |         let ev_content: CallMemberEventContent = | ||||||
|             serde_json::from_value(call_member_ev_json).unwrap(); |             serde_json::from_value(call_member_ev_json).unwrap(); | ||||||
|         assert_eq!(ev_content, call_member_ev); |         assert_eq!( | ||||||
|  |             serde_json::to_string(&ev_content).unwrap(), | ||||||
|  |             serde_json::to_string(&call_member_ev).unwrap() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     #[test] | ||||||
|  |     fn deserialize_member_event() { | ||||||
|  |         let ev = json!({ | ||||||
|  |             "content":{ | ||||||
|  |                 "application": "m.call", | ||||||
|  |                 "call_id": "", | ||||||
|  |                 "scope": "m.room", | ||||||
|  |                 "device_id": "THIS_DEVICE", | ||||||
|  |                 "focus_active":{ | ||||||
|  |                     "type": "livekit", | ||||||
|  |                     "focus_select": "oldest_membership" | ||||||
|  |                 }, | ||||||
|  |                 "foci_preferred": [ | ||||||
|  |                     { | ||||||
|  |                         "livekit_alias": "room1", | ||||||
|  |                         "livekit_service_url": "https://livekit1.com", | ||||||
|  |                         "type": "livekit" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |             "type": "m.call.member", | ||||||
|  |             "origin_server_ts": 111, | ||||||
|  |             "event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc", | ||||||
|  |             "room_id": "!1234:example.org", | ||||||
|  |             "sender": "@user:example.org", | ||||||
|  |             "state_key":"@user:example.org", | ||||||
|  |             "unsigned":{ | ||||||
|  |                 "age":10, | ||||||
|  |                 "prev_content": {}, | ||||||
|  |                 "prev_sender":"@user:example.org", | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         assert_matches!( | ||||||
|  |             from_json_value(ev), | ||||||
|  |             Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event))) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap(); | ||||||
|  |         let sender = OwnedUserId::try_from("@user:example.org").unwrap(); | ||||||
|  |         let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap(); | ||||||
|  |         assert_eq!(member_event.state_key, sender); | ||||||
|  |         assert_eq!(member_event.event_id, event_id); | ||||||
|  |         assert_eq!(member_event.sender, sender); | ||||||
|  |         assert_eq!(member_event.room_id, room_id); | ||||||
|  |         assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap())); | ||||||
|  |         assert_eq!( | ||||||
|  |             member_event.content, | ||||||
|  |             CallMemberEventContent::SessionContent(SessionMembershipData { | ||||||
|  |                 application: Application::Call(CallApplicationContent { | ||||||
|  |                     call_id: "".to_owned(), | ||||||
|  |                     scope: CallScope::Room | ||||||
|  |                 }), | ||||||
|  |                 device_id: "THIS_DEVICE".to_owned(), | ||||||
|  |                 foci_preferred: [Focus::Livekit(LivekitFocus { | ||||||
|  |                     alias: "room1".to_owned(), | ||||||
|  |                     service_url: "https://livekit1.com".to_owned() | ||||||
|  |                 })] | ||||||
|  |                 .to_vec(), | ||||||
|  |                 focus_active: ActiveFocus::Livekit(ActiveLivekitFocus { | ||||||
|  |                     focus_select: FocusSelection::OldestMembership | ||||||
|  |                 }), | ||||||
|  |                 created_ts: None | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(js_int::Int::new(10), member_event.unsigned.age); | ||||||
|  |         assert_eq!( | ||||||
|  |             CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }), | ||||||
|  |             member_event.unsigned.prev_content.unwrap() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // assert_eq!(, StateUnsigned { age: 10, transaction_id: None, prev_content:
 | ||||||
|  |         // CallMemberEventContent::Empty { leave_reason: None }, relations: None })
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn timestamps() -> (TS, TS, TS) { |     fn timestamps() -> (TS, TS, TS) { | ||||||
| @ -449,44 +553,59 @@ mod tests { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn membership_do_expire() { |     fn memberships_do_expire() { | ||||||
|         let content = create_call_member_event_content(); |         let content_legacy = create_call_member_legacy_event_content(); | ||||||
|         let (now, one_second_ago, two_hours_ago) = timestamps(); |         let (now, one_second_ago, two_hours_ago) = timestamps(); | ||||||
|  | 
 | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             content.active_memberships(Some(one_second_ago)), |             content_legacy.active_memberships(Some(one_second_ago)), | ||||||
|             content.memberships.iter().collect::<Vec<&Membership>>() |             content_legacy.memberships() | ||||||
|         ); |         ); | ||||||
|  |         assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships()); | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             content.active_memberships(Some(now)), |             content_legacy.active_memberships(Some(two_hours_ago)), | ||||||
|             content.memberships.iter().collect::<Vec<&Membership>>() |             (vec![] as Vec<MembershipData<'_>>) | ||||||
|  |         ); | ||||||
|  |         // session do never expire
 | ||||||
|  |         let content_session = create_call_member_event_content(); | ||||||
|  |         assert_eq!( | ||||||
|  |             content_session.active_memberships(Some(one_second_ago)), | ||||||
|  |             content_session.memberships() | ||||||
|  |         ); | ||||||
|  |         assert_eq!(content_session.active_memberships(Some(now)), content_session.memberships()); | ||||||
|  |         assert_eq!( | ||||||
|  |             content_session.active_memberships(Some(two_hours_ago)), | ||||||
|  |             content_session.memberships() | ||||||
|         ); |         ); | ||||||
|         assert_eq!(content.active_memberships(Some(two_hours_ago)), vec![] as Vec<&Membership>); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn set_created_ts() { |     fn set_created_ts() { | ||||||
|         let mut content_now = create_call_member_event_content(); |         let mut content_now = create_call_member_legacy_event_content(); | ||||||
|         let mut content_two_hours_ago = create_call_member_event_content(); |         let mut content_two_hours_ago = create_call_member_legacy_event_content(); | ||||||
|         let mut content_one_second_ago = create_call_member_event_content(); |         let mut content_one_second_ago = create_call_member_legacy_event_content(); | ||||||
|         let (now, one_second_ago, two_hours_ago) = timestamps(); |         let (now, one_second_ago, two_hours_ago) = timestamps(); | ||||||
| 
 | 
 | ||||||
|         content_now.set_created_ts_if_none(now); |         content_now.set_created_ts_if_none(now); | ||||||
|         content_one_second_ago.set_created_ts_if_none(one_second_ago); |         content_one_second_ago.set_created_ts_if_none(one_second_ago); | ||||||
|         content_two_hours_ago.set_created_ts_if_none(two_hours_ago); |         content_two_hours_ago.set_created_ts_if_none(two_hours_ago); | ||||||
|         assert_eq!( |         assert_eq!(content_now.active_memberships(None), content_now.memberships()); | ||||||
|             content_now.active_memberships(None), |  | ||||||
|             content_now.memberships.iter().collect::<Vec<&Membership>>() |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         assert_eq!(content_two_hours_ago.active_memberships(None), vec![] as Vec<&Membership>); |         assert_eq!( | ||||||
|  |             content_two_hours_ago.active_memberships(None), | ||||||
|  |             vec![] as Vec<MembershipData<'_>> | ||||||
|  |         ); | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             content_one_second_ago.active_memberships(None), |             content_one_second_ago.active_memberships(None), | ||||||
|             content_one_second_ago.memberships.iter().collect::<Vec<&Membership>>() |             content_one_second_ago.memberships() | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         // created_ts should not be overwritten.
 |         // created_ts should not be overwritten.
 | ||||||
|         content_two_hours_ago.set_created_ts_if_none(one_second_ago); |         content_two_hours_ago.set_created_ts_if_none(one_second_ago); | ||||||
|         // There still should be no active membership.
 |         // There still should be no active membership.
 | ||||||
|         assert_eq!(content_two_hours_ago.active_memberships(None), vec![] as Vec<&Membership>); |         assert_eq!( | ||||||
|  |             content_two_hours_ago.active_memberships(None), | ||||||
|  |             vec![] as Vec<MembershipData<'_>> | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										88
									
								
								crates/ruma-events/src/call/member/focus.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								crates/ruma-events/src/call/member/focus.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | |||||||
|  | //! Types for MatrixRTC Focus/SFU configurations.
 | ||||||
|  | 
 | ||||||
|  | use ruma_macros::StringEnum; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | 
 | ||||||
|  | use crate::PrivOwnedStr; | ||||||
|  | 
 | ||||||
|  | /// Description of the SFU/Focus a membership can be connected to.
 | ||||||
|  | ///
 | ||||||
|  | /// A focus can be any server powering the MatrixRTC session (SFU,
 | ||||||
|  | /// MCU). It serves as a node to redistribute RTC streams.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[serde(tag = "type", rename_all = "snake_case")] | ||||||
|  | pub enum Focus { | ||||||
|  |     /// LiveKit is one possible type of SFU/Focus that can be used for a MatrixRTC session.
 | ||||||
|  |     Livekit(LivekitFocus), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The struct to describe LiveKit as a `preferred_foci`.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub struct LivekitFocus { | ||||||
|  |     /// The alias where the LiveKit sessions can be reached.
 | ||||||
|  |     #[serde(rename = "livekit_alias")] | ||||||
|  |     pub alias: String, | ||||||
|  | 
 | ||||||
|  |     /// The URL of the JWT service for the LiveKit instance.
 | ||||||
|  |     #[serde(rename = "livekit_service_url")] | ||||||
|  |     pub service_url: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl LivekitFocus { | ||||||
|  |     /// Initialize a [`LivekitFocus`].
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Arguments
 | ||||||
|  |     ///
 | ||||||
|  |     /// * `alias` - The alias with which the LiveKit sessions can be reached.
 | ||||||
|  |     /// * `service_url` - The url of the JWT server for the LiveKit instance.
 | ||||||
|  |     pub fn new(alias: String, service_url: String) -> Self { | ||||||
|  |         Self { alias, service_url } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Data to define the actively used Focus.
 | ||||||
|  | ///
 | ||||||
|  | /// A focus can be any server powering the MatrixRTC session (SFU,
 | ||||||
|  | /// MCU). It serves as a node to redistribute RTC streams.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[serde(tag = "type", rename_all = "snake_case")] | ||||||
|  | pub enum ActiveFocus { | ||||||
|  |     /// LiveKit is one possible type of SFU/Focus that can be used for a MatrixRTC session.
 | ||||||
|  |     Livekit(ActiveLivekitFocus), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The fields to describe the `active_foci`.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub struct ActiveLivekitFocus { | ||||||
|  |     /// The selection method used to select the LiveKit focus for the rtc session.
 | ||||||
|  |     pub focus_select: FocusSelection, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ActiveLivekitFocus { | ||||||
|  |     /// Initialize a [`ActiveLivekitFocus`].
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Arguments
 | ||||||
|  |     ///
 | ||||||
|  |     /// * `focus_select` - The selection method used to select the LiveKit focus for the rtc
 | ||||||
|  |     ///   session.
 | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self { focus_select: FocusSelection::OldestMembership } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// How to select the active focus for LiveKit
 | ||||||
|  | #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] | ||||||
|  | #[derive(Clone, PartialEq, StringEnum)] | ||||||
|  | #[ruma_enum(rename_all = "snake_case")] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub enum FocusSelection { | ||||||
|  |     /// Select the active focus by using the oldest membership and the oldest focus.
 | ||||||
|  |     OldestMembership, | ||||||
|  | 
 | ||||||
|  |     #[doc(hidden)] | ||||||
|  |     _Custom(PrivOwnedStr), | ||||||
|  | } | ||||||
							
								
								
									
										319
									
								
								crates/ruma-events/src/call/member/member_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								crates/ruma-events/src/call/member/member_data.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,319 @@ | |||||||
|  | //! Types for MatrixRTC `m.call.member` state event content data ([MSC3401])
 | ||||||
|  | //!
 | ||||||
|  | //! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
 | ||||||
|  | 
 | ||||||
|  | use std::time::Duration; | ||||||
|  | 
 | ||||||
|  | use as_variant::as_variant; | ||||||
|  | use ruma_common::MilliSecondsSinceUnixEpoch; | ||||||
|  | use ruma_macros::StringEnum; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use tracing::warn; | ||||||
|  | 
 | ||||||
|  | use super::focus::{ActiveFocus, ActiveLivekitFocus, Focus}; | ||||||
|  | use crate::PrivOwnedStr; | ||||||
|  | 
 | ||||||
|  | /// The data object that contains the information for one membership.
 | ||||||
|  | ///
 | ||||||
|  | /// It can be a legacy or a normal MatrixRTC Session membership.
 | ||||||
|  | ///
 | ||||||
|  | /// The legacy format contains time information to compute if it is expired or not.
 | ||||||
|  | /// SessionMembershipData does not have the concept of timestamp based expiration anymore.
 | ||||||
|  | /// The state event will reliably be set to empty when the user disconnects.
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | #[cfg_attr(test, derive(PartialEq))] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub enum MembershipData<'a> { | ||||||
|  |     /// The legacy format (using an array of memberships for each device -> one event per user)
 | ||||||
|  |     Legacy(&'a LegacyMembershipData), | ||||||
|  |     /// One event per device. `SessionMembershipData` contains all the information required to
 | ||||||
|  |     /// represent the current membership state of one device.
 | ||||||
|  |     Session(&'a SessionMembershipData), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> MembershipData<'a> { | ||||||
|  |     /// The application this RTC membership participates in (the session type, can be `m.call`...)
 | ||||||
|  |     pub fn application(&self) -> &Application { | ||||||
|  |         match self { | ||||||
|  |             MembershipData::Legacy(data) => &data.application, | ||||||
|  |             MembershipData::Session(data) => &data.application, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// The device id of this membership.
 | ||||||
|  |     pub fn device_id(&self) -> &String { | ||||||
|  |         match self { | ||||||
|  |             MembershipData::Legacy(data) => &data.device_id, | ||||||
|  |             MembershipData::Session(data) => &data.device_id, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// The active focus is a FocusType specific object that describes how this user
 | ||||||
|  |     /// is currently connected.
 | ||||||
|  |     ///
 | ||||||
|  |     /// It can use the foci_preferred list to choose one of the available (preferred)
 | ||||||
|  |     /// foci or specific information on how to connect to this user.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Every user needs to converge to use the same focus_active type.
 | ||||||
|  |     pub fn focus_active(&self) -> &ActiveFocus { | ||||||
|  |         match self { | ||||||
|  |             MembershipData::Legacy(_) => &ActiveFocus::Livekit(ActiveLivekitFocus { | ||||||
|  |                 focus_select: super::focus::FocusSelection::OldestMembership, | ||||||
|  |             }), | ||||||
|  |             MembershipData::Session(data) => &data.focus_active, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// The list of available/preferred options this user provides to connect to the call.
 | ||||||
|  |     pub fn foci_preferred(&self) -> &Vec<Focus> { | ||||||
|  |         match self { | ||||||
|  |             MembershipData::Legacy(data) => &data.foci_active, | ||||||
|  |             MembershipData::Session(data) => &data.foci_preferred, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// The application of the membership is "m.call" and the scope is "m.room".
 | ||||||
|  |     pub fn is_room_call(&self) -> bool { | ||||||
|  |         as_variant!(self.application(), Application::Call) | ||||||
|  |             .is_some_and(|call| call.scope == CallScope::Room) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// The application of the membership is "m.call".
 | ||||||
|  |     pub fn is_call(&self) -> bool { | ||||||
|  |         as_variant!(self.application(), Application::Call).is_some() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Checks if the event is expired. This is only relevant for LegacyMembershipData
 | ||||||
|  |     /// returns `false` if its SessionMembershipData
 | ||||||
|  |     pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool { | ||||||
|  |         match self { | ||||||
|  |             MembershipData::Legacy(data) => data.is_expired(origin_server_ts), | ||||||
|  |             MembershipData::Session(_) => false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Gets the created_ts of the event.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is the `origin_server_ts` for session data.
 | ||||||
|  |     /// For legacy events this can either be the origin server ts or a copy from the
 | ||||||
|  |     /// `origin_server_ts` since we expect legacy events to get updated (when a new device
 | ||||||
|  |     /// joins/leaves).
 | ||||||
|  |     pub fn created_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> { | ||||||
|  |         match self { | ||||||
|  |             MembershipData::Legacy(data) => data.created_ts, | ||||||
|  |             MembershipData::Session(data) => data.created_ts, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// A membership describes one of the sessions this user currently partakes.
 | ||||||
|  | ///
 | ||||||
|  | /// The application defines the type of the session.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub struct LegacyMembershipData { | ||||||
|  |     /// The type of the MatrixRTC session the membership belongs to.
 | ||||||
|  |     ///
 | ||||||
|  |     /// e.g. call, spacial, document...
 | ||||||
|  |     #[serde(flatten)] | ||||||
|  |     pub application: Application, | ||||||
|  | 
 | ||||||
|  |     /// The device id of this membership.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The same user can join with their phone/computer.
 | ||||||
|  |     pub device_id: String, | ||||||
|  | 
 | ||||||
|  |     /// The duration in milliseconds relative to the time this membership joined
 | ||||||
|  |     /// during which the membership is valid.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The time a member has joined is defined as:
 | ||||||
|  |     /// `MIN(content.created_ts, event.origin_server_ts)`
 | ||||||
|  |     #[serde(with = "ruma_common::serde::duration::ms")] | ||||||
|  |     pub expires: Duration, | ||||||
|  | 
 | ||||||
|  |     /// Stores a copy of the `origin_server_ts` of the initial session event.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If the membership is updated this field will be used to track to
 | ||||||
|  |     /// original `origin_server_ts`.
 | ||||||
|  |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub created_ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  | 
 | ||||||
|  |     /// A list of the foci in use for this membership.
 | ||||||
|  |     pub foci_active: Vec<Focus>, | ||||||
|  | 
 | ||||||
|  |     /// The id of the membership.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is required to guarantee uniqueness of the event.
 | ||||||
|  |     /// Sending the same state event twice to synapse makes the HS drop the second one and return
 | ||||||
|  |     /// 200.
 | ||||||
|  |     #[serde(rename = "membershipID")] | ||||||
|  |     pub membership_id: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl LegacyMembershipData { | ||||||
|  |     /// Checks if the event is expired.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Defaults to using `created_ts` of the [`LegacyMembershipData`].
 | ||||||
|  |     /// If no `origin_server_ts` is provided and the event does not contain `created_ts`
 | ||||||
|  |     /// the event will be considered as not expired.
 | ||||||
|  |     /// In this case, a warning will be logged.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Arguments
 | ||||||
|  |     ///
 | ||||||
|  |     /// * `origin_server_ts` - a fallback if [`LegacyMembershipData::created_ts`] is not present
 | ||||||
|  |     pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool { | ||||||
|  |         let ev_created_ts = self.created_ts.or(origin_server_ts); | ||||||
|  | 
 | ||||||
|  |         if let Some(ev_created_ts) = ev_created_ts { | ||||||
|  |             let now = MilliSecondsSinceUnixEpoch::now().to_system_time(); | ||||||
|  |             let expire_ts = ev_created_ts.to_system_time().map(|t| t + self.expires); | ||||||
|  |             now > expire_ts | ||||||
|  |         } else { | ||||||
|  |             // This should not be reached since we only allow events that have copied over
 | ||||||
|  |             // the origin server ts. `set_created_ts_if_none`
 | ||||||
|  |             warn!("Encountered a Call Member state event where the origin_ts (or origin_server_ts) could not be found.\ | ||||||
|  |             It is treated as a non expired event but this might be wrong.");
 | ||||||
|  |             false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Initial set of fields of [`LegacyMembershipData`].
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | #[allow(clippy::exhaustive_structs)] | ||||||
|  | pub struct LegacyMembershipDataInit { | ||||||
|  |     /// The type of the MatrixRTC session the membership belongs to.
 | ||||||
|  |     ///
 | ||||||
|  |     /// e.g. call, spacial, document...
 | ||||||
|  |     pub application: Application, | ||||||
|  | 
 | ||||||
|  |     /// The device id of this membership.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The same user can join with their phone/computer.
 | ||||||
|  |     pub device_id: String, | ||||||
|  | 
 | ||||||
|  |     /// The duration in milliseconds relative to the time this membership joined
 | ||||||
|  |     /// during which the membership is valid.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The time a member has joined is defined as:
 | ||||||
|  |     /// `MIN(content.created_ts, event.origin_server_ts)`
 | ||||||
|  |     pub expires: Duration, | ||||||
|  | 
 | ||||||
|  |     /// A list of the focuses (foci) in use for this membership.
 | ||||||
|  |     pub foci_active: Vec<Focus>, | ||||||
|  | 
 | ||||||
|  |     /// The id of the membership.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is required to guarantee uniqueness of the event.
 | ||||||
|  |     /// Sending the same state event twice to synapse makes the HS drop the second one and return
 | ||||||
|  |     /// 200.
 | ||||||
|  |     pub membership_id: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<LegacyMembershipDataInit> for LegacyMembershipData { | ||||||
|  |     fn from(init: LegacyMembershipDataInit) -> Self { | ||||||
|  |         let LegacyMembershipDataInit { | ||||||
|  |             application, | ||||||
|  |             device_id, | ||||||
|  |             expires, | ||||||
|  |             foci_active, | ||||||
|  |             membership_id, | ||||||
|  |         } = init; | ||||||
|  |         Self { application, device_id, expires, created_ts: None, foci_active, membership_id } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Stores all the information for a MatrixRTC membership. (one for each device)
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub struct SessionMembershipData { | ||||||
|  |     /// The type of the MatrixRTC session the membership belongs to.
 | ||||||
|  |     ///
 | ||||||
|  |     /// e.g. call, spacial, document...
 | ||||||
|  |     #[serde(flatten)] | ||||||
|  |     pub application: Application, | ||||||
|  | 
 | ||||||
|  |     /// The device id of this membership.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The same user can join with their phone/computer.
 | ||||||
|  |     pub device_id: String, | ||||||
|  | 
 | ||||||
|  |     /// A list of the foci that this membership proposes to use.
 | ||||||
|  |     pub foci_preferred: Vec<Focus>, | ||||||
|  | 
 | ||||||
|  |     /// Data required to determine the currently used focus by this member.
 | ||||||
|  |     pub focus_active: ActiveFocus, | ||||||
|  | 
 | ||||||
|  |     /// Stores a copy of the `origin_server_ts` of the initial session event.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is not part of the serialized event and computed after serialization.
 | ||||||
|  |     #[serde(skip)] | ||||||
|  |     pub created_ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The type of the MatrixRTC session.
 | ||||||
|  | ///
 | ||||||
|  | /// This is not the application/client used by the user but the
 | ||||||
|  | /// type of MatrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
 | ||||||
|  | /// possible applications.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[serde(tag = "application")] | ||||||
|  | pub enum Application { | ||||||
|  |     /// The rtc application (session type) for VoIP call.
 | ||||||
|  |     #[serde(rename = "m.call")] | ||||||
|  |     Call(CallApplicationContent), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Call specific parameters of a `m.call.member` event.
 | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | pub struct CallApplicationContent { | ||||||
|  |     /// An identifier for calls.
 | ||||||
|  |     ///
 | ||||||
|  |     /// All members using the same `call_id` will end up in the same call.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Does not need to be a uuid.
 | ||||||
|  |     ///
 | ||||||
|  |     /// `""` is used for room scoped calls.
 | ||||||
|  |     pub call_id: String, | ||||||
|  | 
 | ||||||
|  |     /// Who owns/joins/controls (can modify) the call.
 | ||||||
|  |     pub scope: CallScope, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl CallApplicationContent { | ||||||
|  |     /// Initialize a [`CallApplicationContent`].
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Arguments
 | ||||||
|  |     ///
 | ||||||
|  |     /// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
 | ||||||
|  |     ///   the same call. Does not need to be a uuid. `""` is used for room scoped calls.
 | ||||||
|  |     /// * `scope` - Who owns/joins/controls (can modify) the call.
 | ||||||
|  |     pub fn new(call_id: String, scope: CallScope) -> Self { | ||||||
|  |         Self { call_id, scope } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The call scope defines different call ownership models.
 | ||||||
|  | #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] | ||||||
|  | #[derive(Clone, PartialEq, StringEnum)] | ||||||
|  | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[ruma_enum(rename_all = "m.snake_case")] | ||||||
|  | pub enum CallScope { | ||||||
|  |     /// A call which every user of a room can join and create.
 | ||||||
|  |     ///
 | ||||||
|  |     /// There is no particular name associated with it.
 | ||||||
|  |     ///
 | ||||||
|  |     /// There can only be one per room.
 | ||||||
|  |     Room, | ||||||
|  | 
 | ||||||
|  |     /// A user call is owned by a user.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Each user can create one there can be multiple per room. They are started and ended by the
 | ||||||
|  |     /// owning user.
 | ||||||
|  |     User, | ||||||
|  | 
 | ||||||
|  |     #[doc(hidden)] | ||||||
|  |     _Custom(PrivOwnedStr), | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| //! Type for the matrixRTC notify event ([MSC4075]).
 | //! Type for the MatrixRTC notify event ([MSC4075]).
 | ||||||
| //!
 | //!
 | ||||||
| //! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
 | //! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
 | ||||||
| 
 | 
 | ||||||
| @ -75,3 +75,99 @@ impl From<Application> for ApplicationType { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; | ||||||
|  | 
 | ||||||
|  |     use crate::{ | ||||||
|  |         call::notify::{ApplicationType, CallNotifyEventContent, NotifyType}, | ||||||
|  |         Mentions, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn notify_event_serialization() { | ||||||
|  |         use ruma_common::owned_user_id; | ||||||
|  | 
 | ||||||
|  |         let content_user_mention = CallNotifyEventContent::new( | ||||||
|  |             "abcdef".into(), | ||||||
|  |             ApplicationType::Call, | ||||||
|  |             NotifyType::Ring, | ||||||
|  |             Mentions::with_user_ids(vec![ | ||||||
|  |                 owned_user_id!("@user:example.com"), | ||||||
|  |                 owned_user_id!("@user2:example.com"), | ||||||
|  |             ]), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let content_room_mention = CallNotifyEventContent::new( | ||||||
|  |             "abcdef".into(), | ||||||
|  |             ApplicationType::Call, | ||||||
|  |             NotifyType::Ring, | ||||||
|  |             Mentions::with_room_mention(), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_eq!( | ||||||
|  |             to_json_value(&content_user_mention).unwrap(), | ||||||
|  |             json!({ | ||||||
|  |                 "call_id": "abcdef", | ||||||
|  |                 "application": "m.call", | ||||||
|  |                 "m.mentions": { | ||||||
|  |                     "user_ids": ["@user2:example.com","@user:example.com"], | ||||||
|  |                 }, | ||||||
|  |                 "notify_type": "ring", | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  |         assert_eq!( | ||||||
|  |             to_json_value(&content_room_mention).unwrap(), | ||||||
|  |             json!({ | ||||||
|  |                 "call_id": "abcdef", | ||||||
|  |                 "application": "m.call", | ||||||
|  |                 "m.mentions": { "room": true }, | ||||||
|  |                 "notify_type": "ring", | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn notify_event_deserialization() { | ||||||
|  |         use std::collections::BTreeSet; | ||||||
|  | 
 | ||||||
|  |         use assert_matches2::assert_matches; | ||||||
|  |         use ruma_common::owned_user_id; | ||||||
|  | 
 | ||||||
|  |         use crate::{AnyMessageLikeEvent, MessageLikeEvent}; | ||||||
|  | 
 | ||||||
|  |         let json_data = json!({ | ||||||
|  |             "content": { | ||||||
|  |                 "call_id": "abcdef", | ||||||
|  |                 "application": "m.call", | ||||||
|  |                 "m.mentions": { | ||||||
|  |                     "room": false, | ||||||
|  |                     "user_ids": ["@user:example.com", "@user2:example.com"], | ||||||
|  |                 }, | ||||||
|  |                 "notify_type": "ring", | ||||||
|  |             }, | ||||||
|  |             "event_id": "$event:notareal.hs", | ||||||
|  |             "origin_server_ts": 134_829_848, | ||||||
|  |             "room_id": "!roomid:notareal.hs", | ||||||
|  |             "sender": "@user:notareal.hs", | ||||||
|  |             "type": "m.call.notify", | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap(); | ||||||
|  |         assert_matches!( | ||||||
|  |             event, | ||||||
|  |             AnyMessageLikeEvent::CallNotify(MessageLikeEvent::Original(message_event)) | ||||||
|  |         ); | ||||||
|  |         let content = message_event.content; | ||||||
|  |         assert_eq!(content.call_id, "abcdef"); | ||||||
|  |         assert!(!content.mentions.room); | ||||||
|  |         assert_eq!( | ||||||
|  |             content.mentions.user_ids, | ||||||
|  |             BTreeSet::from([ | ||||||
|  |                 owned_user_id!("@user:example.com"), | ||||||
|  |                 owned_user_id!("@user2:example.com") | ||||||
|  |             ]) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| //! Types for the [`m.call.sdp_stream_metadata_changed`] event.
 | //! Types for the [`m.call.sdp_stream_metadata_changed`] event.
 | ||||||
| //!
 | //!
 | ||||||
| //! [`m.call.sdp_stream_metadata_changed`]: https://github.com/matrix-org/matrix-spec-proposals/pull/3291
 | //! [`m.call.sdp_stream_metadata_changed`]: https://spec.matrix.org/latest/client-server-api/#mcallsdp_stream_metadata_changed
 | ||||||
| 
 | 
 | ||||||
| use std::collections::BTreeMap; | use std::collections::BTreeMap; | ||||||
| 
 | 
 | ||||||
| @ -15,7 +15,7 @@ use super::StreamMetadata; | |||||||
| /// This event is sent by any party when a stream metadata changes but no negotiation is required.
 | /// This event is sent by any party when a stream metadata changes but no negotiation is required.
 | ||||||
| #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] | #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| #[ruma_event(type = "org.matrix.call.sdp_stream_metadata_changed", kind = MessageLike)] | #[ruma_event(type = "m.call.sdp_stream_metadata_changed", alias = "org.matrix.call.sdp_stream_metadata_changed", kind = MessageLike)] | ||||||
| pub struct CallSdpStreamMetadataChangedEventContent { | pub struct CallSdpStreamMetadataChangedEventContent { | ||||||
|     /// A unique identifier for the call.
 |     /// A unique identifier for the call.
 | ||||||
|     pub call_id: OwnedVoipId, |     pub call_id: OwnedVoipId, | ||||||
|  | |||||||
| @ -95,7 +95,6 @@ pub trait ToDeviceEventContent: EventContent<EventType = ToDeviceEventType> {} | |||||||
| /// Event content that can be deserialized with its event type.
 | /// Event content that can be deserialized with its event type.
 | ||||||
| pub trait EventContentFromType: EventContent { | pub trait EventContentFromType: EventContent { | ||||||
|     /// Constructs this event content from the given event type and JSON.
 |     /// Constructs this event content from the given event type and JSON.
 | ||||||
|     #[doc(hidden)] |  | ||||||
|     fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self>; |     fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -45,9 +45,8 @@ event_enum! { | |||||||
|         "m.call.candidates" => super::call::candidates, |         "m.call.candidates" => super::call::candidates, | ||||||
|         "m.call.negotiate" => super::call::negotiate, |         "m.call.negotiate" => super::call::negotiate, | ||||||
|         "m.call.reject" => super::call::reject, |         "m.call.reject" => super::call::reject, | ||||||
|         #[cfg(feature = "unstable-msc3291")] |         #[ruma_enum(alias = "org.matrix.call.sdp_stream_metadata_changed")] | ||||||
|         #[ruma_enum(alias = "m.call.sdp_stream_metadata_changed")] |         "m.call.sdp_stream_metadata_changed" => super::call::sdp_stream_metadata_changed, | ||||||
|         "org.matrix.call.sdp_stream_metadata_changed" => super::call::sdp_stream_metadata_changed, |  | ||||||
|         "m.call.select_answer" => super::call::select_answer, |         "m.call.select_answer" => super::call::select_answer, | ||||||
|         #[cfg(feature = "unstable-msc3954")] |         #[cfg(feature = "unstable-msc3954")] | ||||||
|         #[ruma_enum(alias = "m.emote")] |         #[ruma_enum(alias = "m.emote")] | ||||||
| @ -88,6 +87,9 @@ event_enum! { | |||||||
|         #[cfg(feature = "unstable-msc3381")] |         #[cfg(feature = "unstable-msc3381")] | ||||||
|         #[ruma_enum(ident = UnstablePollEnd)] |         #[ruma_enum(ident = UnstablePollEnd)] | ||||||
|         "org.matrix.msc3381.poll.end" => super::poll::unstable_end, |         "org.matrix.msc3381.poll.end" => super::poll::unstable_end, | ||||||
|  |         #[cfg(feature = "unstable-msc3489")] | ||||||
|  |         #[ruma_enum(alias = "m.beacon")] | ||||||
|  |         "org.matrix.msc3672.beacon" => super::beacon, | ||||||
|         "m.reaction" => super::reaction, |         "m.reaction" => super::reaction, | ||||||
|         "m.room.encrypted" => super::room::encrypted, |         "m.room.encrypted" => super::room::encrypted, | ||||||
|         "m.room.message" => super::room::message, |         "m.room.message" => super::room::message, | ||||||
| @ -127,6 +129,9 @@ event_enum! { | |||||||
|         "m.room.topic" => super::room::topic, |         "m.room.topic" => super::room::topic, | ||||||
|         "m.space.child" => super::space::child, |         "m.space.child" => super::space::child, | ||||||
|         "m.space.parent" => super::space::parent, |         "m.space.parent" => super::space::parent, | ||||||
|  |         #[cfg(feature = "unstable-msc3489")] | ||||||
|  |         #[ruma_enum(alias = "m.beacon_info")] | ||||||
|  |         "org.matrix.msc3672.beacon_info" => super::beacon_info, | ||||||
|         #[cfg(feature = "unstable-msc3401")] |         #[cfg(feature = "unstable-msc3401")] | ||||||
|         #[ruma_enum(alias = "m.call.member")] |         #[ruma_enum(alias = "m.call.member")] | ||||||
|         "org.matrix.msc3401.call.member" => super::call::member, |         "org.matrix.msc3401.call.member" => super::call::member, | ||||||
| @ -309,6 +314,8 @@ impl AnyMessageLikeEventContent { | |||||||
|     /// This is a helper function intended for encryption. There should not be a reason to access
 |     /// This is a helper function intended for encryption. There should not be a reason to access
 | ||||||
|     /// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise.
 |     /// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise.
 | ||||||
|     pub fn relation(&self) -> Option<encrypted::Relation> { |     pub fn relation(&self) -> Option<encrypted::Relation> { | ||||||
|  |         #[cfg(feature = "unstable-msc3489")] | ||||||
|  |         use super::beacon::BeaconEventContent; | ||||||
|         use super::key::verification::{ |         use super::key::verification::{ | ||||||
|             accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent, |             accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent, | ||||||
|             done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent, |             done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent, | ||||||
| @ -361,13 +368,16 @@ impl AnyMessageLikeEventContent { | |||||||
|             | Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => { |             | Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => { | ||||||
|                 Some(encrypted::Relation::Reference(relates_to.clone())) |                 Some(encrypted::Relation::Reference(relates_to.clone())) | ||||||
|             } |             } | ||||||
|  |             #[cfg(feature = "unstable-msc3489")] | ||||||
|  |             Self::Beacon(BeaconEventContent { relates_to, .. }) => { | ||||||
|  |                 Some(encrypted::Relation::Reference(relates_to.clone())) | ||||||
|  |             } | ||||||
|             #[cfg(feature = "unstable-msc3381")] |             #[cfg(feature = "unstable-msc3381")] | ||||||
|             Self::PollStart(_) | Self::UnstablePollStart(_) => None, |             Self::PollStart(_) | Self::UnstablePollStart(_) => None, | ||||||
|             #[cfg(feature = "unstable-msc4075")] |             #[cfg(feature = "unstable-msc4075")] | ||||||
|             Self::CallNotify(_) => None, |             Self::CallNotify(_) => None, | ||||||
|             #[cfg(feature = "unstable-msc3291")] |             Self::CallSdpStreamMetadataChanged(_) | ||||||
|             Self::CallSdpStreamMetadataChanged(_) => None, |             | Self::CallNegotiate(_) | ||||||
|             Self::CallNegotiate(_) |  | ||||||
|             | Self::CallReject(_) |             | Self::CallReject(_) | ||||||
|             | Self::CallSelectAnswer(_) |             | Self::CallSelectAnswer(_) | ||||||
|             | Self::CallAnswer(_) |             | Self::CallAnswer(_) | ||||||
|  | |||||||
| @ -165,7 +165,10 @@ mod tests { | |||||||
|     use std::collections::BTreeMap; |     use std::collections::BTreeMap; | ||||||
| 
 | 
 | ||||||
|     use assert_matches2::assert_matches; |     use assert_matches2::assert_matches; | ||||||
|     use ruma_common::{event_id, serde::Base64}; |     use ruma_common::{ | ||||||
|  |         event_id, | ||||||
|  |         serde::{Base64, Raw}, | ||||||
|  |     }; | ||||||
|     use serde_json::{ |     use serde_json::{ | ||||||
|         from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, |         from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, | ||||||
|     }; |     }; | ||||||
| @ -348,4 +351,26 @@ mod tests { | |||||||
|         assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2); |         assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2); | ||||||
|         assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); |         assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn in_room_serialization_roundtrip() { | ||||||
|  |         let event_id = event_id!("$1598361704261elfgc:localhost"); | ||||||
|  | 
 | ||||||
|  |         let content = KeyVerificationAcceptEventContent { | ||||||
|  |             relates_to: Reference { event_id: event_id.to_owned() }, | ||||||
|  |             method: AcceptMethod::SasV1(SasV1Content { | ||||||
|  |                 hash: HashAlgorithm::Sha256, | ||||||
|  |                 key_agreement_protocol: KeyAgreementProtocol::Curve25519, | ||||||
|  |                 message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2, | ||||||
|  |                 short_authentication_string: vec![ShortAuthenticationString::Decimal], | ||||||
|  |                 commitment: Base64::new(b"hello".to_vec()), | ||||||
|  |             }), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let json_content = Raw::new(&content).unwrap(); | ||||||
|  |         let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |         assert_matches!(deser_content.method, AcceptMethod::SasV1(_)); | ||||||
|  |         assert_eq!(deser_content.relates_to.event_id, event_id); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -139,6 +139,10 @@ pub mod macros { | |||||||
| 
 | 
 | ||||||
| #[cfg(feature = "unstable-msc3927")] | #[cfg(feature = "unstable-msc3927")] | ||||||
| pub mod audio; | pub mod audio; | ||||||
|  | #[cfg(feature = "unstable-msc3489")] | ||||||
|  | pub mod beacon; | ||||||
|  | #[cfg(feature = "unstable-msc3489")] | ||||||
|  | pub mod beacon_info; | ||||||
| pub mod call; | pub mod call; | ||||||
| pub mod direct; | pub mod direct; | ||||||
| pub mod dummy; | pub mod dummy; | ||||||
|  | |||||||
| @ -37,7 +37,7 @@ impl From<Annotation> for ReactionEventContent { | |||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use assert_matches2::assert_matches; |     use assert_matches2::assert_matches; | ||||||
|     use ruma_common::owned_event_id; |     use ruma_common::{owned_event_id, serde::Raw}; | ||||||
|     use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; |     use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; | ||||||
| 
 | 
 | ||||||
|     use super::ReactionEventContent; |     use super::ReactionEventContent; | ||||||
| @ -79,4 +79,18 @@ mod tests { | |||||||
|             }) |             }) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn serialization_roundtrip() { | ||||||
|  |         let content = ReactionEventContent::new(Annotation::new( | ||||||
|  |             owned_event_id!("$my_reaction"), | ||||||
|  |             "🏠".to_owned(), | ||||||
|  |         )); | ||||||
|  | 
 | ||||||
|  |         let json_content = Raw::new(&content).unwrap(); | ||||||
|  |         let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(deser_content.relates_to.event_id, content.relates_to.event_id); | ||||||
|  |         assert_eq!(deser_content.relates_to.key, content.relates_to.key); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -165,6 +165,7 @@ impl<C> From<message::Relation<C>> for Relation { | |||||||
| /// [replaces another event]: https://spec.matrix.org/latest/client-server-api/#event-replacements
 | /// [replaces another event]: https://spec.matrix.org/latest/client-server-api/#event-replacements
 | ||||||
| #[derive(Clone, Debug, Deserialize, Serialize)] | #[derive(Clone, Debug, Deserialize, Serialize)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  | #[serde(tag = "rel_type", rename = "m.replace")] | ||||||
| pub struct Replacement { | pub struct Replacement { | ||||||
|     /// The ID of the event being replaced.
 |     /// The ID of the event being replaced.
 | ||||||
|     pub event_id: OwnedEventId, |     pub event_id: OwnedEventId, | ||||||
|  | |||||||
| @ -74,27 +74,11 @@ impl Serialize for Relation { | |||||||
|                 st.serialize_field("m.in_reply_to", in_reply_to)?; |                 st.serialize_field("m.in_reply_to", in_reply_to)?; | ||||||
|                 st.end() |                 st.end() | ||||||
|             } |             } | ||||||
|             Relation::Replacement(data) => { |             Relation::Replacement(data) => data.serialize(serializer), | ||||||
|                 RelationSerHelper { rel_type: "m.replace", data }.serialize(serializer) |             Relation::Reference(data) => data.serialize(serializer), | ||||||
|             } |             Relation::Annotation(data) => data.serialize(serializer), | ||||||
|             Relation::Reference(data) => { |             Relation::Thread(data) => data.serialize(serializer), | ||||||
|                 RelationSerHelper { rel_type: "m.reference", data }.serialize(serializer) |  | ||||||
|             } |  | ||||||
|             Relation::Annotation(data) => { |  | ||||||
|                 RelationSerHelper { rel_type: "m.annotation", data }.serialize(serializer) |  | ||||||
|             } |  | ||||||
|             Relation::Thread(data) => { |  | ||||||
|                 RelationSerHelper { rel_type: "m.thread", data }.serialize(serializer) |  | ||||||
|             } |  | ||||||
|             Relation::_Custom(c) => c.serialize(serializer), |             Relation::_Custom(c) => c.serialize(serializer), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct RelationSerHelper<'a, T> { |  | ||||||
|     rel_type: &'a str, |  | ||||||
| 
 |  | ||||||
|     #[serde(flatten)] |  | ||||||
|     data: &'a T, |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -168,6 +168,7 @@ impl From<JoinRule> for SpaceRoomJoinRule { | |||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| pub struct Restricted { | pub struct Restricted { | ||||||
|     /// Allow rules which describe conditions that allow joining a room.
 |     /// Allow rules which describe conditions that allow joining a room.
 | ||||||
|  |     #[serde(default)] | ||||||
|     pub allow: Vec<AllowRule>, |     pub allow: Vec<AllowRule>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -324,6 +325,16 @@ mod tests { | |||||||
|         assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json); |         assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn restricted_room_no_allow_field() { | ||||||
|  |         let json = r#"{"join_rule":"restricted"}"#; | ||||||
|  |         let join_rules: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap(); | ||||||
|  |         assert_matches!( | ||||||
|  |             join_rules, | ||||||
|  |             RoomJoinRulesEventContent { join_rule: JoinRule::Restricted(_) } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn join_rule_to_space_room_join_rule() { |     fn join_rule_to_space_room_join_rule() { | ||||||
|         assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into()); |         assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into()); | ||||||
|  | |||||||
| @ -157,7 +157,7 @@ pub(super) enum RelationSerHelper { | |||||||
|     Replacement(ReplacementJsonRepr), |     Replacement(ReplacementJsonRepr), | ||||||
| 
 | 
 | ||||||
|     /// An event that belongs to a thread, with stable names.
 |     /// An event that belongs to a thread, with stable names.
 | ||||||
|     #[serde(rename = "m.thread")] |     #[serde(untagged)] | ||||||
|     Thread(Thread), |     Thread(Thread), | ||||||
| 
 | 
 | ||||||
|     /// An unknown relation type.
 |     /// An unknown relation type.
 | ||||||
|  | |||||||
| @ -57,7 +57,7 @@ fn is_default_bits(val: &UInt) -> bool { | |||||||
| ///
 | ///
 | ||||||
| /// The only algorithm currently specified is `m.secret_storage.v1.aes-hmac-sha2`, so this
 | /// The only algorithm currently specified is `m.secret_storage.v1.aes-hmac-sha2`, so this
 | ||||||
| /// essentially represents `AesHmacSha2KeyDescription` in the
 | /// essentially represents `AesHmacSha2KeyDescription` in the
 | ||||||
| /// [spec](https://spec.matrix.org/latest/client-server-api/#msecret_storagev1aes-hmac-sha2).
 | /// [spec](https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2).
 | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| #[derive(Clone, Debug, Serialize, EventContent)] | #[derive(Clone, Debug, Serialize, EventContent)] | ||||||
| #[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)] | #[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)] | ||||||
| @ -137,7 +137,7 @@ impl SecretStorageEncryptionAlgorithm { | |||||||
| /// The key properties for the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
 | /// The key properties for the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
 | ||||||
| ///
 | ///
 | ||||||
| /// Corresponds to the AES-specific properties of `AesHmacSha2KeyDescription` in the
 | /// Corresponds to the AES-specific properties of `AesHmacSha2KeyDescription` in the
 | ||||||
| /// [spec](https://spec.matrix.org/latest/client-server-api/#msecret_storagev1aes-hmac-sha2).
 | /// [spec](https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2).
 | ||||||
| #[derive(Debug, Clone, Deserialize, Serialize)] | #[derive(Debug, Clone, Deserialize, Serialize)] | ||||||
| #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
| pub struct SecretStorageV1AesHmacSha2Properties { | pub struct SecretStorageV1AesHmacSha2Properties { | ||||||
| @ -182,7 +182,7 @@ mod tests { | |||||||
|         PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent, |         PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent, | ||||||
|         SecretStorageV1AesHmacSha2Properties, |         SecretStorageV1AesHmacSha2Properties, | ||||||
|     }; |     }; | ||||||
|     use crate::{EventContentFromType, GlobalAccountDataEvent}; |     use crate::{AnyGlobalAccountDataEvent, EventContentFromType, GlobalAccountDataEvent}; | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn key_description_serialization() { |     fn key_description_serialization() { | ||||||
| @ -326,7 +326,7 @@ mod tests { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn event_serialization() { |     fn event_content_serialization() { | ||||||
|         let mut content = SecretStorageKeyEventContent::new( |         let mut content = SecretStorageKeyEventContent::new( | ||||||
|             "my_key_id".into(), |             "my_key_id".into(), | ||||||
|             SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties { |             SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties { | ||||||
| @ -346,6 +346,31 @@ mod tests { | |||||||
|         assert_eq!(to_json_value(&content).unwrap(), json); |         assert_eq!(to_json_value(&content).unwrap(), json); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn event_serialization() { | ||||||
|  |         let mut content = SecretStorageKeyEventContent::new( | ||||||
|  |             "my_key_id".into(), | ||||||
|  |             SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties { | ||||||
|  |                 iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()), | ||||||
|  |                 mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()), | ||||||
|  |             }), | ||||||
|  |         ); | ||||||
|  |         content.name = Some("my_key".to_owned()); | ||||||
|  |         let event = GlobalAccountDataEvent { content }; | ||||||
|  | 
 | ||||||
|  |         let json = json!({ | ||||||
|  |             "type": "m.secret_storage.key.my_key_id", | ||||||
|  |             "content": { | ||||||
|  |                 "name": "my_key", | ||||||
|  |                 "algorithm": "m.secret_storage.v1.aes-hmac-sha2", | ||||||
|  |                 "iv": "YWJjZGVmZ2hpamtsbW5vcA", | ||||||
|  |                 "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(to_json_value(&event).unwrap(), json); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn event_deserialization() { |     fn event_deserialization() { | ||||||
|         let json = json!({ |         let json = json!({ | ||||||
| @ -358,8 +383,8 @@ mod tests { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         let ev = |         let any_ev = from_json_value::<AnyGlobalAccountDataEvent>(json).unwrap(); | ||||||
|             from_json_value::<GlobalAccountDataEvent<SecretStorageKeyEventContent>>(json).unwrap(); |         assert_matches!(any_ev, AnyGlobalAccountDataEvent::SecretStorageKey(ev)); | ||||||
|         assert_eq!(ev.content.key_id, "my_key_id"); |         assert_eq!(ev.content.key_id, "my_key_id"); | ||||||
|         assert_eq!(ev.content.name.unwrap(), "my_key"); |         assert_eq!(ev.content.name.unwrap(), "my_key"); | ||||||
|         assert_matches!(ev.content.passphrase, None); |         assert_matches!(ev.content.passphrase, None); | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								crates/ruma-events/tests/it/beacon.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								crates/ruma-events/tests/it/beacon.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | #![cfg(feature = "unstable-msc3489")] | ||||||
|  | 
 | ||||||
|  | use assert_matches2::assert_matches; | ||||||
|  | use js_int::uint; | ||||||
|  | use ruma_common::{ | ||||||
|  |     owned_event_id, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch, | ||||||
|  | }; | ||||||
|  | use ruma_events::{ | ||||||
|  |     beacon::BeaconEventContent, relation::Reference, AnyMessageLikeEvent, MessageLikeEvent, | ||||||
|  | }; | ||||||
|  | use serde_json::{ | ||||||
|  |     from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | fn get_beacon_event_content() -> BeaconEventContent { | ||||||
|  |     BeaconEventContent::new( | ||||||
|  |         owned_event_id!("$beacon_info_event_id:example.com"), | ||||||
|  |         "geo:51.5008,0.1247;u=35".to_owned(), | ||||||
|  |         Some(MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))), | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn get_beacon_event_content_json() -> JsonValue { | ||||||
|  |     json!({ | ||||||
|  |         "m.relates_to": { | ||||||
|  |             "rel_type": "m.reference", | ||||||
|  |             "event_id": "$beacon_info_event_id:example.com" | ||||||
|  |         }, | ||||||
|  |         "org.matrix.msc3488.location": { | ||||||
|  |             "uri": "geo:51.5008,0.1247;u=35", | ||||||
|  |         }, | ||||||
|  |         "org.matrix.msc3488.ts": 1_636_829_458 | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_event_content_serialization() { | ||||||
|  |     let event_content = get_beacon_event_content(); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(to_json_value(&event_content).unwrap(), get_beacon_event_content_json()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_event_content_deserialization() { | ||||||
|  |     let json_data = get_beacon_event_content_json(); | ||||||
|  | 
 | ||||||
|  |     let event_content: BeaconEventContent = | ||||||
|  |         from_json_value::<BeaconEventContent>(json_data).unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_eq!( | ||||||
|  |         event_content.relates_to.event_id, | ||||||
|  |         owned_event_id!("$beacon_info_event_id:example.com") | ||||||
|  |     ); | ||||||
|  |     assert_eq!(event_content.location.uri, "geo:51.5008,0.1247;u=35"); | ||||||
|  |     assert_eq!(event_content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn message_event_deserialization() { | ||||||
|  |     let json_data = json!({ | ||||||
|  |         "content": get_beacon_event_content_json(), | ||||||
|  |         "event_id": "$beacon_event_id:example.com", | ||||||
|  |         "origin_server_ts": 1_636_829_458, | ||||||
|  |         "room_id": "!roomid:example.com", | ||||||
|  |         "type": "org.matrix.msc3672.beacon", | ||||||
|  |         "sender": "@example:example.com" | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(event, AnyMessageLikeEvent::Beacon(MessageLikeEvent::Original(ev))); | ||||||
|  |     assert_eq!(ev.content.location.uri, "geo:51.5008,0.1247;u=35"); | ||||||
|  |     assert_eq!(ev.content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); | ||||||
|  |     assert_matches!(ev.content.relates_to, Reference { event_id, .. }); | ||||||
|  |     assert_eq!(event_id, owned_event_id!("$beacon_info_event_id:example.com")); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(ev.sender, user_id!("@example:example.com")); | ||||||
|  |     assert_eq!(ev.room_id, room_id!("!roomid:example.com")); | ||||||
|  |     assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); | ||||||
|  |     assert!(ev.unsigned.is_empty()); | ||||||
|  | } | ||||||
							
								
								
									
										161
									
								
								crates/ruma-events/tests/it/beacon_info.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								crates/ruma-events/tests/it/beacon_info.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | |||||||
|  | #![cfg(feature = "unstable-msc3489")] | ||||||
|  | 
 | ||||||
|  | use std::time::Duration; | ||||||
|  | 
 | ||||||
|  | use assert_matches2::assert_matches; | ||||||
|  | use js_int::uint; | ||||||
|  | use ruma_common::{event_id, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch}; | ||||||
|  | use ruma_events::{ | ||||||
|  |     beacon_info::BeaconInfoEventContent, location::AssetType, AnyStateEvent, StateEvent, | ||||||
|  | }; | ||||||
|  | use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; | ||||||
|  | 
 | ||||||
|  | fn get_beacon_info_event_content( | ||||||
|  |     duration: Option<Duration>, | ||||||
|  |     ts: Option<MilliSecondsSinceUnixEpoch>, | ||||||
|  | ) -> BeaconInfoEventContent { | ||||||
|  |     let description = Some("Kylie's live location".to_owned()); | ||||||
|  |     let duration_or = duration.unwrap_or(Duration::from_secs(60)); | ||||||
|  |     let ts_or = Some(ts.unwrap_or(MilliSecondsSinceUnixEpoch::now())); | ||||||
|  | 
 | ||||||
|  |     BeaconInfoEventContent::new(description, duration_or, true, ts_or) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn get_beacon_info_json() -> serde_json::Value { | ||||||
|  |     json!({ | ||||||
|  |         "org.matrix.msc3488.ts": 1_636_829_458, | ||||||
|  |         "org.matrix.msc3488.asset": { | ||||||
|  |             "type": "m.self" | ||||||
|  |         }, | ||||||
|  |         "timeout": 60_000, | ||||||
|  |         "description": "Kylie's live location", | ||||||
|  |         "live": true | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_info_is_live() { | ||||||
|  |     let event_content = get_beacon_info_event_content(None, None); | ||||||
|  | 
 | ||||||
|  |     assert!(event_content.is_live()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_info_is_not_live() { | ||||||
|  |     let duration = Some(Duration::from_nanos(1)); | ||||||
|  |     let event_content = get_beacon_info_event_content(duration, None); | ||||||
|  | 
 | ||||||
|  |     assert!(!event_content.is_live()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_info_stop_event() { | ||||||
|  |     let ts = Some(MilliSecondsSinceUnixEpoch(1_636_829_458_u64.try_into().unwrap())); | ||||||
|  | 
 | ||||||
|  |     let mut event_content = get_beacon_info_event_content(None, ts); | ||||||
|  | 
 | ||||||
|  |     event_content.stop(); | ||||||
|  | 
 | ||||||
|  |     assert_eq!( | ||||||
|  |         to_json_value(&event_content).unwrap(), | ||||||
|  |         json!({ | ||||||
|  |             "org.matrix.msc3488.ts": 1_636_829_458, | ||||||
|  |             "org.matrix.msc3488.asset": { | ||||||
|  |                 "type": "m.self" | ||||||
|  |             }, | ||||||
|  |             "timeout": 60_000, | ||||||
|  |             "description": "Kylie's live location", | ||||||
|  |             "live": false | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_info_start_event() { | ||||||
|  |     let ts = Some(MilliSecondsSinceUnixEpoch(1_636_829_458_u64.try_into().unwrap())); | ||||||
|  | 
 | ||||||
|  |     let mut event_content = BeaconInfoEventContent::new( | ||||||
|  |         Some("Kylie's live location".to_owned()), | ||||||
|  |         Duration::from_secs(60), | ||||||
|  |         false, | ||||||
|  |         ts, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     event_content.start(); | ||||||
|  | 
 | ||||||
|  |     assert_eq!( | ||||||
|  |         to_json_value(&event_content).unwrap(), | ||||||
|  |         json!({ | ||||||
|  |             "org.matrix.msc3488.ts": 1_636_829_458, | ||||||
|  |             "org.matrix.msc3488.asset": { | ||||||
|  |                 "type": "m.self" | ||||||
|  |             }, | ||||||
|  |             "timeout": 60_000, | ||||||
|  |             "description": "Kylie's live location", | ||||||
|  |             "live": true | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_info_start_event_content_serialization() { | ||||||
|  |     let ts = Some(MilliSecondsSinceUnixEpoch(1_636_829_458_u64.try_into().unwrap())); | ||||||
|  | 
 | ||||||
|  |     let event_content = get_beacon_info_event_content(None, ts); | ||||||
|  | 
 | ||||||
|  |     assert_eq!( | ||||||
|  |         to_json_value(&event_content).unwrap(), | ||||||
|  |         json!({ | ||||||
|  |             "org.matrix.msc3488.ts": 1_636_829_458, | ||||||
|  |             "org.matrix.msc3488.asset": { | ||||||
|  |                 "type": "m.self" | ||||||
|  |             }, | ||||||
|  |             "timeout": 60_000, | ||||||
|  |             "description": "Kylie's live location", | ||||||
|  |             "live": true | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn beacon_info_start_event_content_deserialization() { | ||||||
|  |     let json_data = get_beacon_info_json(); | ||||||
|  | 
 | ||||||
|  |     let event_content: BeaconInfoEventContent = serde_json::from_value(json_data).unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(event_content.description, Some("Kylie's live location".to_owned())); | ||||||
|  |     assert!(event_content.live); | ||||||
|  |     assert_eq!(event_content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); | ||||||
|  |     assert_eq!(event_content.timeout, Duration::from_secs(60)); | ||||||
|  |     assert_eq!(event_content.asset.type_, AssetType::Self_); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn state_event_deserialization() { | ||||||
|  |     let json_data = json!({ | ||||||
|  |         "content": get_beacon_info_json(), | ||||||
|  |         "event_id": "$beacon_event_id:example.com", | ||||||
|  |         "origin_server_ts": 1_636_829_458, | ||||||
|  |         "room_id": "!roomid:example.com", | ||||||
|  |         "type": "org.matrix.msc3672.beacon_info", | ||||||
|  |         "sender": "@example:example.com", | ||||||
|  |         "state_key": "@example:example.com" | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let event = from_json_value::<AnyStateEvent>(json_data).unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(event, AnyStateEvent::BeaconInfo(StateEvent::Original(ev))); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(ev.content.description, Some("Kylie's live location".to_owned())); | ||||||
|  |     assert_eq!(ev.content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); | ||||||
|  |     assert_eq!(ev.content.timeout, Duration::from_secs(60)); | ||||||
|  |     assert_eq!(ev.content.asset.type_, AssetType::Self_); | ||||||
|  |     assert!(ev.content.live); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(ev.event_id, event_id!("$beacon_event_id:example.com")); | ||||||
|  |     assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); | ||||||
|  |     assert_eq!(ev.room_id, room_id!("!roomid:example.com")); | ||||||
|  |     assert_eq!(ev.sender, user_id!("@example:example.com")); | ||||||
|  |     assert_eq!(ev.state_key, "@example:example.com"); | ||||||
|  |     assert!(ev.unsigned.is_empty()); | ||||||
|  | } | ||||||
| @ -1,6 +1,3 @@ | |||||||
| #[cfg(feature = "unstable-msc4075")] |  | ||||||
| use std::collections::BTreeSet; |  | ||||||
| 
 |  | ||||||
| use assert_matches2::assert_matches; | use assert_matches2::assert_matches; | ||||||
| #[cfg(feature = "unstable-msc2747")] | #[cfg(feature = "unstable-msc2747")] | ||||||
| use assign::assign; | use assign::assign; | ||||||
| @ -8,11 +5,6 @@ use js_int::uint; | |||||||
| use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId}; | use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId}; | ||||||
| #[cfg(feature = "unstable-msc2747")] | #[cfg(feature = "unstable-msc2747")] | ||||||
| use ruma_events::call::CallCapabilities; | use ruma_events::call::CallCapabilities; | ||||||
| #[cfg(feature = "unstable-msc4075")] |  | ||||||
| use ruma_events::{ |  | ||||||
|     call::notify::{ApplicationType, CallNotifyEventContent, NotifyType}, |  | ||||||
|     Mentions, |  | ||||||
| }; |  | ||||||
| use ruma_events::{ | use ruma_events::{ | ||||||
|     call::{ |     call::{ | ||||||
|         answer::CallAnswerEventContent, |         answer::CallAnswerEventContent, | ||||||
| @ -616,83 +608,3 @@ fn select_v1_answer_event_deserialization() { | |||||||
|     assert_eq!(content.selected_party_id, "6336"); |     assert_eq!(content.selected_party_id, "6336"); | ||||||
|     assert_eq!(content.version, VoipVersionId::V1); |     assert_eq!(content.version, VoipVersionId::V1); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| #[cfg(feature = "unstable-msc4075")] |  | ||||||
| #[test] |  | ||||||
| fn notify_event_serialization() { |  | ||||||
|     use ruma_common::owned_user_id; |  | ||||||
| 
 |  | ||||||
|     let content_user_mention = CallNotifyEventContent::new( |  | ||||||
|         "abcdef".into(), |  | ||||||
|         ApplicationType::Call, |  | ||||||
|         NotifyType::Ring, |  | ||||||
|         Mentions::with_user_ids(vec![ |  | ||||||
|             owned_user_id!("@user:example.com"), |  | ||||||
|             owned_user_id!("@user2:example.com"), |  | ||||||
|         ]), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     let content_room_mention = CallNotifyEventContent::new( |  | ||||||
|         "abcdef".into(), |  | ||||||
|         ApplicationType::Call, |  | ||||||
|         NotifyType::Ring, |  | ||||||
|         Mentions::with_room_mention(), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     assert_eq!( |  | ||||||
|         to_json_value(&content_user_mention).unwrap(), |  | ||||||
|         json!({ |  | ||||||
|             "call_id": "abcdef", |  | ||||||
|             "application": "m.call", |  | ||||||
|             "m.mentions": { |  | ||||||
|                 "user_ids": ["@user2:example.com","@user:example.com"], |  | ||||||
|             }, |  | ||||||
|             "notify_type": "ring", |  | ||||||
|         }) |  | ||||||
|     ); |  | ||||||
|     assert_eq!( |  | ||||||
|         to_json_value(&content_room_mention).unwrap(), |  | ||||||
|         json!({ |  | ||||||
|             "call_id": "abcdef", |  | ||||||
|             "application": "m.call", |  | ||||||
|             "m.mentions": { "room": true }, |  | ||||||
|             "notify_type": "ring", |  | ||||||
|         }) |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[cfg(feature = "unstable-msc4075")] |  | ||||||
| #[test] |  | ||||||
| fn notify_event_deserialization() { |  | ||||||
|     use ruma_common::owned_user_id; |  | ||||||
| 
 |  | ||||||
|     let json_data = json!({ |  | ||||||
|         "content": { |  | ||||||
|             "call_id": "abcdef", |  | ||||||
|             "application": "m.call", |  | ||||||
|             "m.mentions": { |  | ||||||
|                 "room": false, |  | ||||||
|                 "user_ids": ["@user:example.com", "@user2:example.com"], |  | ||||||
|             }, |  | ||||||
|             "notify_type": "ring", |  | ||||||
|         }, |  | ||||||
|         "event_id": "$event:notareal.hs", |  | ||||||
|         "origin_server_ts": 134_829_848, |  | ||||||
|         "room_id": "!roomid:notareal.hs", |  | ||||||
|         "sender": "@user:notareal.hs", |  | ||||||
|         "type": "m.call.notify", |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap(); |  | ||||||
|     assert_matches!( |  | ||||||
|         event, |  | ||||||
|         AnyMessageLikeEvent::CallNotify(MessageLikeEvent::Original(message_event)) |  | ||||||
|     ); |  | ||||||
|     let content = message_event.content; |  | ||||||
|     assert_eq!(content.call_id, "abcdef"); |  | ||||||
|     assert!(!content.mentions.room); |  | ||||||
|     assert_eq!( |  | ||||||
|         content.mentions.user_ids, |  | ||||||
|         BTreeSet::from([owned_user_id!("@user:example.com"), owned_user_id!("@user2:example.com")]) |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| use assert_matches2::assert_matches; | use assert_matches2::assert_matches; | ||||||
| use ruma_common::{owned_device_id, owned_event_id}; | use ruma_common::{owned_device_id, owned_event_id, serde::Raw}; | ||||||
| use ruma_events::{ | use ruma_events::{ | ||||||
|     relation::{CustomRelation, InReplyTo, Reference, Thread}, |     relation::{Annotation, CustomRelation, InReplyTo, Reference, Thread}, | ||||||
|     room::encrypted::{ |     room::encrypted::{ | ||||||
|         EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, |         EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, | ||||||
|         RoomEncryptedEventContent, |         RoomEncryptedEventContent, | ||||||
| @ -82,6 +82,17 @@ fn content_no_relation_deserialization() { | |||||||
|     assert_matches!(content.relates_to, None); |     assert_matches!(content.relates_to, None); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn content_no_relation_serialization_roundtrip() { | ||||||
|  |     let content = RoomEncryptedEventContent::new(encrypted_scheme(), None); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     assert_matches!(deser_content.relates_to, None); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn content_reply_serialization() { | fn content_reply_serialization() { | ||||||
|     let content = RoomEncryptedEventContent::new( |     let content = RoomEncryptedEventContent::new( | ||||||
| @ -149,6 +160,22 @@ fn content_reply_deserialization() { | |||||||
|     assert_eq!(in_reply_to.event_id, "$replied_to_event"); |     assert_eq!(in_reply_to.event_id, "$replied_to_event"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn content_reply_serialization_roundtrip() { | ||||||
|  |     let reply = InReplyTo::new(owned_event_id!("$replied_to_event")); | ||||||
|  |     let content = RoomEncryptedEventContent::new( | ||||||
|  |         encrypted_scheme(), | ||||||
|  |         Some(Relation::Reply { in_reply_to: reply.clone() }), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     assert_matches!(deser_content.relates_to, Some(Relation::Reply { in_reply_to: deser_reply })); | ||||||
|  |     assert_eq!(deser_reply.event_id, reply.event_id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn content_replacement_serialization() { | fn content_replacement_serialization() { | ||||||
|     let content = RoomEncryptedEventContent::new( |     let content = RoomEncryptedEventContent::new( | ||||||
| @ -214,6 +241,22 @@ fn content_replacement_deserialization() { | |||||||
|     assert_eq!(replacement.event_id, "$replaced_event"); |     assert_eq!(replacement.event_id, "$replaced_event"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn content_replacement_serialization_roundtrip() { | ||||||
|  |     let replacement = Replacement::new(owned_event_id!("$replaced_event")); | ||||||
|  |     let content = RoomEncryptedEventContent::new( | ||||||
|  |         encrypted_scheme(), | ||||||
|  |         Some(Relation::Replacement(replacement.clone())), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     assert_matches!(deser_content.relates_to, Some(Relation::Replacement(deser_replacement))); | ||||||
|  |     assert_eq!(deser_replacement.event_id, replacement.event_id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn content_reference_serialization() { | fn content_reference_serialization() { | ||||||
|     let content = RoomEncryptedEventContent::new( |     let content = RoomEncryptedEventContent::new( | ||||||
| @ -279,6 +322,22 @@ fn content_reference_deserialization() { | |||||||
|     assert_eq!(reference.event_id, "$referenced_event"); |     assert_eq!(reference.event_id, "$referenced_event"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn content_reference_serialization_roundtrip() { | ||||||
|  |     let reference = Reference::new(owned_event_id!("$referenced_event")); | ||||||
|  |     let content = RoomEncryptedEventContent::new( | ||||||
|  |         encrypted_scheme(), | ||||||
|  |         Some(Relation::Reference(reference.clone())), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     assert_matches!(deser_content.relates_to, Some(Relation::Reference(deser_reference))); | ||||||
|  |     assert_eq!(deser_reference.event_id, reference.event_id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn content_thread_serialization() { | fn content_thread_serialization() { | ||||||
|     let content = RoomEncryptedEventContent::new( |     let content = RoomEncryptedEventContent::new( | ||||||
| @ -357,9 +416,23 @@ fn content_thread_deserialization() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[test] | #[test] | ||||||
| fn content_annotation_serialization() { | fn content_thread_serialization_roundtrip() { | ||||||
|     use ruma_events::relation::Annotation; |     let thread = Thread::plain(owned_event_id!("$thread_root"), owned_event_id!("$prev_event")); | ||||||
|  |     let content = | ||||||
|  |         RoomEncryptedEventContent::new(encrypted_scheme(), Some(Relation::Thread(thread.clone()))); | ||||||
| 
 | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     assert_matches!(deser_content.relates_to, Some(Relation::Thread(deser_thread))); | ||||||
|  |     assert_eq!(deser_thread.event_id, thread.event_id); | ||||||
|  |     assert_eq!(deser_thread.in_reply_to.unwrap().event_id, thread.in_reply_to.unwrap().event_id); | ||||||
|  |     assert_eq!(deser_thread.is_falling_back, thread.is_falling_back); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn content_annotation_serialization() { | ||||||
|     let content = RoomEncryptedEventContent::new( |     let content = RoomEncryptedEventContent::new( | ||||||
|         encrypted_scheme(), |         encrypted_scheme(), | ||||||
|         Some(Relation::Annotation(Annotation::new( |         Some(Relation::Annotation(Annotation::new( | ||||||
| @ -429,6 +502,23 @@ fn content_annotation_deserialization() { | |||||||
|     assert_eq!(annotation.key, "some_key"); |     assert_eq!(annotation.key, "some_key"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn content_annotation_serialization_roundtrip() { | ||||||
|  |     let annotation = Annotation::new(owned_event_id!("$annotated_event"), "some_key".to_owned()); | ||||||
|  |     let content = RoomEncryptedEventContent::new( | ||||||
|  |         encrypted_scheme(), | ||||||
|  |         Some(Relation::Annotation(annotation.clone())), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     assert_matches!(deser_content.relates_to, Some(Relation::Annotation(deser_annotation))); | ||||||
|  |     assert_eq!(deser_annotation.event_id, annotation.event_id); | ||||||
|  |     assert_eq!(deser_annotation.key, annotation.key); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn custom_relation_deserialization() { | fn custom_relation_deserialization() { | ||||||
|     let relation_json = json!({ |     let relation_json = json!({ | ||||||
| @ -502,3 +592,31 @@ fn custom_relation_serialization() { | |||||||
|         }) |         }) | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn custom_serialization_roundtrip() { | ||||||
|  |     let rel_type = "io.ruma.unknown"; | ||||||
|  |     let event_id = "$related_event"; | ||||||
|  |     let key = "value"; | ||||||
|  |     let json_relation = json!({ | ||||||
|  |         "rel_type": rel_type, | ||||||
|  |         "event_id": event_id, | ||||||
|  |         "key": key, | ||||||
|  |     }); | ||||||
|  |     let relation = from_json_value::<CustomRelation>(json_relation).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let content = | ||||||
|  |         RoomEncryptedEventContent::new(encrypted_scheme(), Some(Relation::_Custom(relation))); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_)); | ||||||
|  |     let deser_relates_to = deser_content.relates_to.unwrap(); | ||||||
|  |     assert_matches!(&deser_relates_to, Relation::_Custom(_)); | ||||||
|  |     assert_eq!(deser_relates_to.rel_type().unwrap().as_str(), rel_type); | ||||||
|  |     let deser_relation = deser_relates_to.data(); | ||||||
|  |     assert_eq!(deser_relation.get("rel_type").unwrap().as_str().unwrap(), rel_type); | ||||||
|  |     assert_eq!(deser_relation.get("event_id").unwrap().as_str().unwrap(), event_id); | ||||||
|  |     assert_eq!(deser_relation.get("key").unwrap().as_str().unwrap(), key); | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,8 +1,15 @@ | |||||||
| use assert_matches2::assert_matches; | use assert_matches2::assert_matches; | ||||||
| use js_int::uint; | use js_int::uint; | ||||||
| use ruma_common::{serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId}; | use ruma_common::{ | ||||||
| use ruma_events::{AnyMessageLikeEvent, MessageLikeEvent}; |     serde::{CanBeEmpty, Raw}, | ||||||
| use serde_json::{from_value as from_json_value, json}; |     MilliSecondsSinceUnixEpoch, VoipVersionId, | ||||||
|  | }; | ||||||
|  | use ruma_events::{ | ||||||
|  |     secret_storage::key::{SecretStorageEncryptionAlgorithm, SecretStorageV1AesHmacSha2Properties}, | ||||||
|  |     AnyGlobalAccountDataEventContent, AnyMessageLikeEvent, AnyMessageLikeEventContent, | ||||||
|  |     MessageLikeEvent, RawExt as _, | ||||||
|  | }; | ||||||
|  | use serde_json::{from_value as from_json_value, json, value::to_raw_value as to_raw_json_value}; | ||||||
| 
 | 
 | ||||||
| #[test] | #[test] | ||||||
| fn ui() { | fn ui() { | ||||||
| @ -46,3 +53,53 @@ fn deserialize_message_event() { | |||||||
|     assert_eq!(content.call_id, "foofoo"); |     assert_eq!(content.call_id, "foofoo"); | ||||||
|     assert_eq!(content.version, VoipVersionId::V0); |     assert_eq!(content.version, VoipVersionId::V0); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn text_msgtype_plain_text_deserialization_as_any() { | ||||||
|  |     let serialized = json!({ | ||||||
|  |         "body": "Hello world!", | ||||||
|  |         "msgtype": "m.text" | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let raw_event: Raw<AnyMessageLikeEventContent> = | ||||||
|  |         Raw::from_json_string(serialized.to_string()).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let event = raw_event.deserialize_with_type("m.room.message".into()).unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(event, AnyMessageLikeEventContent::RoomMessage(content)); | ||||||
|  |     assert_eq!(content.body(), "Hello world!"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn secret_storage_key_deserialization_as_any() { | ||||||
|  |     let serialized = to_raw_json_value(&json!({ | ||||||
|  |         "name": "my_key", | ||||||
|  |         "algorithm": "m.secret_storage.v1.aes-hmac-sha2", | ||||||
|  |         "iv": "YWJjZGVmZ2hpamtsbW5vcA", | ||||||
|  |         "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" | ||||||
|  |     })) | ||||||
|  |     .unwrap(); | ||||||
|  | 
 | ||||||
|  |     let raw_event: Raw<AnyGlobalAccountDataEventContent> = | ||||||
|  |         Raw::from_json_string(serialized.to_string()).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let event = raw_event.deserialize_with_type("m.secret_storage.key.test".into()).unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(event, AnyGlobalAccountDataEventContent::SecretStorageKey(content)); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(content.name.unwrap(), "my_key"); | ||||||
|  |     assert_eq!(content.key_id, "test"); | ||||||
|  |     assert_matches!(content.passphrase, None); | ||||||
|  | 
 | ||||||
|  |     assert_matches!( | ||||||
|  |         content.algorithm, | ||||||
|  |         SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties { | ||||||
|  |             iv: Some(iv), | ||||||
|  |             mac: Some(mac), | ||||||
|  |             .. | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA"); | ||||||
|  |     assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"); | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,4 +1,6 @@ | |||||||
| mod audio; | mod audio; | ||||||
|  | mod beacon; | ||||||
|  | mod beacon_info; | ||||||
| mod call; | mod call; | ||||||
| mod encrypted; | mod encrypted; | ||||||
| mod enums; | mod enums; | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| use assert_matches2::assert_matches; | use assert_matches2::assert_matches; | ||||||
| use assign::assign; | use assign::assign; | ||||||
| use ruma_common::owned_event_id; | use ruma_common::{owned_event_id, serde::Raw}; | ||||||
| use ruma_events::{ | use ruma_events::{ | ||||||
|     relation::{CustomRelation, InReplyTo, Replacement, Thread}, |     relation::{CustomRelation, InReplyTo, Replacement, Thread}, | ||||||
|     room::message::{MessageType, Relation, RoomMessageEventContent}, |     room::message::{MessageType, Relation, RoomMessageEventContent}, | ||||||
| @ -52,6 +52,22 @@ fn reply_serialize() { | |||||||
|     ); |     ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn reply_serialization_roundtrip() { | ||||||
|  |     let body = "This is a reply"; | ||||||
|  |     let mut content = RoomMessageEventContent::text_plain(body); | ||||||
|  |     let reply = InReplyTo::new(owned_event_id!("$1598361704261elfgc")); | ||||||
|  |     content.relates_to = Some(Relation::Reply { in_reply_to: reply.clone() }); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg)); | ||||||
|  |     assert_eq!(deser_msg.body, body); | ||||||
|  |     assert_matches!(content.relates_to.unwrap(), Relation::Reply { in_reply_to: deser_reply }); | ||||||
|  |     assert_eq!(deser_reply.event_id, reply.event_id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn replacement_serialize() { | fn replacement_serialize() { | ||||||
|     let content = assign!( |     let content = assign!( | ||||||
| @ -111,6 +127,28 @@ fn replacement_deserialize() { | |||||||
|     assert_eq!(text.body, "Hello! My name is bar"); |     assert_eq!(text.body, "Hello! My name is bar"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn replacement_serialization_roundtrip() { | ||||||
|  |     let body = "<text msg>"; | ||||||
|  |     let mut content = RoomMessageEventContent::text_plain(body); | ||||||
|  |     let new_body = "This is the new content."; | ||||||
|  |     let replacement = Replacement::new( | ||||||
|  |         owned_event_id!("$1598361704261elfgc"), | ||||||
|  |         RoomMessageEventContent::text_plain(new_body).into(), | ||||||
|  |     ); | ||||||
|  |     content.relates_to = Some(Relation::Replacement(replacement.clone())); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg)); | ||||||
|  |     assert_eq!(deser_msg.body, body); | ||||||
|  |     assert_matches!(content.relates_to.unwrap(), Relation::Replacement(deser_replacement)); | ||||||
|  |     assert_eq!(deser_replacement.event_id, replacement.event_id); | ||||||
|  |     assert_matches!(deser_replacement.new_content.msgtype, MessageType::Text(deser_new_msg)); | ||||||
|  |     assert_eq!(deser_new_msg.body, new_body); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn thread_plain_serialize() { | fn thread_plain_serialize() { | ||||||
|     let content = assign!( |     let content = assign!( | ||||||
| @ -250,6 +288,25 @@ fn thread_unstable_deserialize() { | |||||||
|     assert!(!thread.is_falling_back); |     assert!(!thread.is_falling_back); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn thread_serialization_roundtrip() { | ||||||
|  |     let body = "<text msg>"; | ||||||
|  |     let mut content = RoomMessageEventContent::text_plain(body); | ||||||
|  |     let thread = | ||||||
|  |         Thread::plain(owned_event_id!("$1598361704261elfgc"), owned_event_id!("$latesteventid")); | ||||||
|  |     content.relates_to = Some(Relation::Thread(thread.clone())); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg)); | ||||||
|  |     assert_eq!(deser_msg.body, body); | ||||||
|  |     assert_matches!(content.relates_to.unwrap(), Relation::Thread(deser_thread)); | ||||||
|  |     assert_eq!(deser_thread.event_id, thread.event_id); | ||||||
|  |     assert_eq!(deser_thread.in_reply_to.unwrap().event_id, thread.in_reply_to.unwrap().event_id); | ||||||
|  |     assert_eq!(deser_thread.is_falling_back, thread.is_falling_back); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn custom_deserialize() { | fn custom_deserialize() { | ||||||
|     let relation_json = json!({ |     let relation_json = json!({ | ||||||
| @ -300,3 +357,33 @@ fn custom_serialize() { | |||||||
|         }) |         }) | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn custom_serialization_roundtrip() { | ||||||
|  |     let rel_type = "io.ruma.unknown"; | ||||||
|  |     let event_id = "$related_event"; | ||||||
|  |     let key = "value"; | ||||||
|  |     let json_relation = json!({ | ||||||
|  |         "rel_type": rel_type, | ||||||
|  |         "event_id": event_id, | ||||||
|  |         "key": key, | ||||||
|  |     }); | ||||||
|  |     let relation = from_json_value::<CustomRelation>(json_relation).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let body = "<text msg>"; | ||||||
|  |     let mut content = RoomMessageEventContent::text_plain(body); | ||||||
|  |     content.relates_to = Some(Relation::_Custom(relation)); | ||||||
|  | 
 | ||||||
|  |     let json_content = Raw::new(&content).unwrap(); | ||||||
|  |     let deser_content = json_content.deserialize().unwrap(); | ||||||
|  | 
 | ||||||
|  |     assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg)); | ||||||
|  |     assert_eq!(deser_msg.body, body); | ||||||
|  |     let deser_relates_to = deser_content.relates_to.unwrap(); | ||||||
|  |     assert_matches!(&deser_relates_to, Relation::_Custom(_)); | ||||||
|  |     assert_eq!(deser_relates_to.rel_type().unwrap().as_str(), rel_type); | ||||||
|  |     let deser_relation = deser_relates_to.data(); | ||||||
|  |     assert_eq!(deser_relation.get("rel_type").unwrap().as_str().unwrap(), rel_type); | ||||||
|  |     assert_eq!(deser_relation.get("event_id").unwrap().as_str().unwrap(), event_id); | ||||||
|  |     assert_eq!(deser_relation.get("key").unwrap().as_str().unwrap(), key); | ||||||
|  | } | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ use ruma_events::{ | |||||||
|         }, |         }, | ||||||
|         EncryptedFileInit, JsonWebKeyInit, MediaSource, |         EncryptedFileInit, JsonWebKeyInit, MediaSource, | ||||||
|     }, |     }, | ||||||
|     AnySyncTimelineEvent, Mentions, MessageLikeUnsigned, |     AnySyncTimelineEvent, EventContent, Mentions, MessageLikeUnsigned, RawExt, | ||||||
| }; | }; | ||||||
| use serde_json::{ | use serde_json::{ | ||||||
|     from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, |     from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, | ||||||
| @ -484,6 +484,49 @@ fn reply_thread_fallback() { | |||||||
|     assert!(thread_info.is_falling_back); |     assert!(thread_info.is_falling_back); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[test] | ||||||
|  | fn reply_thread_serialization_roundtrip() { | ||||||
|  |     let thread_root = OriginalRoomMessageEvent { | ||||||
|  |         content: RoomMessageEventContent::text_plain("Thread root"), | ||||||
|  |         event_id: owned_event_id!("$thread_root"), | ||||||
|  |         origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), | ||||||
|  |         room_id: owned_room_id!("!testroomid:example.org"), | ||||||
|  |         sender: owned_user_id!("@user:example.org"), | ||||||
|  |         unsigned: MessageLikeUnsigned::default(), | ||||||
|  |     }; | ||||||
|  |     let threaded_message = OriginalRoomMessageEvent { | ||||||
|  |         content: RoomMessageEventContent::text_plain("Threaded message").make_for_thread( | ||||||
|  |             &thread_root, | ||||||
|  |             ReplyWithinThread::No, | ||||||
|  |             AddMentions::No, | ||||||
|  |         ), | ||||||
|  |         event_id: owned_event_id!("$threaded_message"), | ||||||
|  |         origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), | ||||||
|  |         room_id: owned_room_id!("!testroomid:example.org"), | ||||||
|  |         sender: owned_user_id!("@user:example.org"), | ||||||
|  |         unsigned: MessageLikeUnsigned::default(), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let reply_as_thread_fallback = RoomMessageEventContent::text_plain( | ||||||
|  |         "Reply from a thread client", | ||||||
|  |     ) | ||||||
|  |     .make_reply_to(&threaded_message, ForwardThread::Yes, AddMentions::No); | ||||||
|  | 
 | ||||||
|  |     let as_raw = Raw::new(&reply_as_thread_fallback).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let reply_as_thread_fallback = | ||||||
|  |         as_raw.deserialize_with_type(reply_as_thread_fallback.event_type()).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let relation = reply_as_thread_fallback.relates_to.unwrap(); | ||||||
|  |     assert_matches!(relation, Relation::Thread(thread_info)); | ||||||
|  |     assert_eq!( | ||||||
|  |         thread_info.in_reply_to.map(|in_reply_to| in_reply_to.event_id), | ||||||
|  |         Some(threaded_message.event_id) | ||||||
|  |     ); | ||||||
|  |     assert_eq!(thread_info.event_id, thread_root.event_id); | ||||||
|  |     assert!(thread_info.is_falling_back); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[test] | #[test] | ||||||
| fn reply_add_mentions() { | fn reply_add_mentions() { | ||||||
|     let user = owned_user_id!("@user:example.org"); |     let user = owned_user_id!("@user:example.org"); | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ pub mod v1 { | |||||||
|         pub query_type: String, |         pub query_type: String, | ||||||
| 
 | 
 | ||||||
|         /// The query parameters.
 |         /// The query parameters.
 | ||||||
|         #[ruma_api(query_map)] |         #[ruma_api(query_all)] | ||||||
|         pub params: BTreeMap<String, String>, |         pub params: BTreeMap<String, String>, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,13 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | Breaking Changes: | ||||||
|  | 
 | ||||||
|  | - `MatrixElement::Div` is now a newtype variant. | ||||||
|  | 
 | ||||||
|  | Improvements: | ||||||
|  | 
 | ||||||
|  | - Add support for mathematical messages, according to MSC2191 / Matrix 1.11 | ||||||
|  | 
 | ||||||
| # 0.2.0 | # 0.2.0 | ||||||
| 
 | 
 | ||||||
| Breaking Changes: | Breaking Changes: | ||||||
|  | |||||||
| @ -152,7 +152,7 @@ pub enum MatrixElement { | |||||||
|     /// [`<div>`], a content division element.
 |     /// [`<div>`], a content division element.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// [`<div>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
 |     /// [`<div>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
 | ||||||
|     Div, |     Div(DivData), | ||||||
| 
 | 
 | ||||||
|     /// [`<table>`], a table element.
 |     /// [`<table>`], a table element.
 | ||||||
|     ///
 |     ///
 | ||||||
| @ -268,7 +268,10 @@ impl MatrixElement { | |||||||
|             } |             } | ||||||
|             b"hr" => (Self::Hr, attrs.clone()), |             b"hr" => (Self::Hr, attrs.clone()), | ||||||
|             b"br" => (Self::Br, attrs.clone()), |             b"br" => (Self::Br, attrs.clone()), | ||||||
|             b"div" => (Self::Div, attrs.clone()), |             b"div" => { | ||||||
|  |                 let (data, attrs) = DivData::parse(attrs); | ||||||
|  |                 (Self::Div(data), attrs) | ||||||
|  |             } | ||||||
|             b"table" => (Self::Table, attrs.clone()), |             b"table" => (Self::Table, attrs.clone()), | ||||||
|             b"thead" => (Self::Thead, attrs.clone()), |             b"thead" => (Self::Thead, attrs.clone()), | ||||||
|             b"tbody" => (Self::Tbody, attrs.clone()), |             b"tbody" => (Self::Tbody, attrs.clone()), | ||||||
| @ -599,12 +602,23 @@ pub struct SpanData { | |||||||
|     ///
 |     ///
 | ||||||
|     /// [spoiler message]: https://spec.matrix.org/latest/client-server-api/#spoiler-messages
 |     /// [spoiler message]: https://spec.matrix.org/latest/client-server-api/#spoiler-messages
 | ||||||
|     pub spoiler: Option<StrTendril>, |     pub spoiler: Option<StrTendril>, | ||||||
|  | 
 | ||||||
|  |     /// `data-mx-maths`, an inline Matrix [mathematical message].
 | ||||||
|  |     ///
 | ||||||
|  |     /// The value is the mathematical notation in [LaTeX] format.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If this attribute is present, the content of the span is the fallback representation of the
 | ||||||
|  |     /// mathematical notation.
 | ||||||
|  |     ///
 | ||||||
|  |     /// [mathematical message]: https://spec.matrix.org/latest/client-server-api/#mathematical-messages
 | ||||||
|  |     /// [LaTeX]: https://www.latex-project.org/
 | ||||||
|  |     pub maths: Option<StrTendril>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl SpanData { | impl SpanData { | ||||||
|     /// Construct an empty `SpanData`.
 |     /// Construct an empty `SpanData`.
 | ||||||
|     fn new() -> Self { |     fn new() -> Self { | ||||||
|         Self { bg_color: None, color: None, spoiler: None } |         Self { bg_color: None, color: None, spoiler: None, maths: None } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Parse the given attributes to construct a new `SpanData`.
 |     /// Parse the given attributes to construct a new `SpanData`.
 | ||||||
| @ -629,6 +643,9 @@ impl SpanData { | |||||||
|                 b"data-mx-spoiler" => { |                 b"data-mx-spoiler" => { | ||||||
|                     data.spoiler = Some(attr.value.clone()); |                     data.spoiler = Some(attr.value.clone()); | ||||||
|                 } |                 } | ||||||
|  |                 b"data-mx-maths" => { | ||||||
|  |                     data.maths = Some(attr.value.clone()); | ||||||
|  |                 } | ||||||
|                 _ => { |                 _ => { | ||||||
|                     remaining_attrs.insert(attr.clone()); |                     remaining_attrs.insert(attr.clone()); | ||||||
|                 } |                 } | ||||||
| @ -722,3 +739,53 @@ impl ImageData { | |||||||
|         (data, remaining_attrs) |         (data, remaining_attrs) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /// The supported data of a `<div>` HTML element.
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | #[non_exhaustive] | ||||||
|  | pub struct DivData { | ||||||
|  |     /// `data-mx-maths`, a Matrix [mathematical message] block.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The value is the mathematical notation in [LaTeX] format.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If this attribute is present, the content of the div is the fallback representation of the
 | ||||||
|  |     /// mathematical notation.
 | ||||||
|  |     ///
 | ||||||
|  |     /// [mathematical message]: https://spec.matrix.org/latest/client-server-api/#mathematical-messages
 | ||||||
|  |     /// [LaTeX]: https://www.latex-project.org/
 | ||||||
|  |     pub maths: Option<StrTendril>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl DivData { | ||||||
|  |     /// Construct an empty `DivData`.
 | ||||||
|  |     fn new() -> Self { | ||||||
|  |         Self { maths: None } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Parse the given attributes to construct a new `SpanData`.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
 | ||||||
|  |     #[allow(clippy::mutable_key_type)] | ||||||
|  |     fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) { | ||||||
|  |         let mut data = Self::new(); | ||||||
|  |         let mut remaining_attrs = BTreeSet::new(); | ||||||
|  | 
 | ||||||
|  |         for attr in attrs { | ||||||
|  |             if attr.name.ns != ns!() { | ||||||
|  |                 remaining_attrs.insert(attr.clone()); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             match attr.name.local.as_bytes() { | ||||||
|  |                 b"data-mx-maths" => { | ||||||
|  |                     data.maths = Some(attr.value.clone()); | ||||||
|  |                 } | ||||||
|  |                 _ => { | ||||||
|  |                     remaining_attrs.insert(attr.clone()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         (data, remaining_attrs) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -28,14 +28,16 @@ static ALLOWED_ATTRIBUTES_STRICT: Map<&str, &Set<&str>> = phf_map! { | |||||||
|     "img" => &ALLOWED_ATTRIBUTES_IMG_STRICT, |     "img" => &ALLOWED_ATTRIBUTES_IMG_STRICT, | ||||||
|     "ol" => &ALLOWED_ATTRIBUTES_OL_STRICT, |     "ol" => &ALLOWED_ATTRIBUTES_OL_STRICT, | ||||||
|     "code" => &ALLOWED_ATTRIBUTES_CODE_STRICT, |     "code" => &ALLOWED_ATTRIBUTES_CODE_STRICT, | ||||||
|  |     "div" => &ALLOWED_ATTRIBUTES_DIV_STRICT, | ||||||
| }; | }; | ||||||
| static ALLOWED_ATTRIBUTES_SPAN_STRICT: Set<&str> = | static ALLOWED_ATTRIBUTES_SPAN_STRICT: Set<&str> = | ||||||
|     phf_set! { "data-mx-bg-color", "data-mx-color", "data-mx-spoiler" }; |     phf_set! { "data-mx-bg-color", "data-mx-color", "data-mx-spoiler", "data-mx-maths" }; | ||||||
| static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" }; | static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" }; | ||||||
| static ALLOWED_ATTRIBUTES_IMG_STRICT: Set<&str> = | static ALLOWED_ATTRIBUTES_IMG_STRICT: Set<&str> = | ||||||
|     phf_set! { "width", "height", "alt", "title", "src" }; |     phf_set! { "width", "height", "alt", "title", "src" }; | ||||||
| static ALLOWED_ATTRIBUTES_OL_STRICT: Set<&str> = phf_set! { "start" }; | static ALLOWED_ATTRIBUTES_OL_STRICT: Set<&str> = phf_set! { "start" }; | ||||||
| static ALLOWED_ATTRIBUTES_CODE_STRICT: Set<&str> = phf_set! { "class" }; | static ALLOWED_ATTRIBUTES_CODE_STRICT: Set<&str> = phf_set! { "class" }; | ||||||
|  | static ALLOWED_ATTRIBUTES_DIV_STRICT: Set<&str> = phf_set! { "data-mx-maths" }; | ||||||
| 
 | 
 | ||||||
| /// Attributes that were previously allowed on HTML elements according to the Matrix specification,
 | /// Attributes that were previously allowed on HTML elements according to the Matrix specification,
 | ||||||
| /// with their replacement.
 | /// with their replacement.
 | ||||||
|  | |||||||
| @ -26,7 +26,8 @@ fn elements() { | |||||||
|     // `<div>` element.
 |     // `<div>` element.
 | ||||||
|     let div_node = html_children.next().unwrap(); |     let div_node = html_children.next().unwrap(); | ||||||
|     let div_element = div_node.as_element().unwrap().to_matrix(); |     let div_element = div_node.as_element().unwrap().to_matrix(); | ||||||
|     assert_matches!(div_element.element, MatrixElement::Div); |     assert_matches!(div_element.element, MatrixElement::Div(div)); | ||||||
|  |     assert_eq!(div.maths, None); | ||||||
|     // The `class` attribute is not supported.
 |     // The `class` attribute is not supported.
 | ||||||
|     assert_eq!(div_element.attrs.len(), 1); |     assert_eq!(div_element.attrs.len(), 1); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,10 +5,8 @@ | |||||||
| Bug fixes: | Bug fixes: | ||||||
| 
 | 
 | ||||||
| - Allow underscores (`_`) when validating MXC URIs. | - Allow underscores (`_`) when validating MXC URIs. | ||||||
|   - They have always been allowed in [the spec][mxc validation spec] |   - They have always been allowed in the spec in order to support URL-safe | ||||||
|     in order to support URL-safe base64-encoded media IDs. |     base64-encoded media IDs. | ||||||
| 
 |  | ||||||
| [mxc validation spec]: https://spec.matrix.org/v1.9/client-server-api/#security-considerations-5 |  | ||||||
| 
 | 
 | ||||||
| Improvements: | Improvements: | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -77,7 +77,7 @@ pub enum MxcUriError { | |||||||
|     /// Media identifier malformed due to invalid characters detected.
 |     /// Media identifier malformed due to invalid characters detected.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// Valid characters are (in regex notation) `[A-Za-z0-9_-]+`.
 |     /// Valid characters are (in regex notation) `[A-Za-z0-9_-]+`.
 | ||||||
|     /// See [here](https://spec.matrix.org/v1.10/client-server-api/#security-considerations-5) for more details.
 |     /// See [here](https://spec.matrix.org/v1.11/client-server-api/#security-considerations-5) for more details.
 | ||||||
|     #[error("Media Identifier malformed, invalid characters")] |     #[error("Media Identifier malformed, invalid characters")] | ||||||
|     MediaIdMalformed, |     MediaIdMalformed, | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ pub fn validate(uri: &str) -> Result<NonZeroU8, MxcUriError> { | |||||||
| 
 | 
 | ||||||
|     let server_name = &uri[..index]; |     let server_name = &uri[..index]; | ||||||
|     let media_id = &uri[index + 1..]; |     let media_id = &uri[index + 1..]; | ||||||
|     // See: https://spec.matrix.org/v1.10/client-server-api/#security-considerations-5
 |     // See: https://spec.matrix.org/v1.11/client-server-api/#security-considerations-5
 | ||||||
|     let media_id_is_valid = media_id |     let media_id_is_valid = media_id | ||||||
|         .bytes() |         .bytes() | ||||||
|         .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_' )); |         .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_' )); | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ mod kw { | |||||||
|     syn::custom_keyword!(raw_body); |     syn::custom_keyword!(raw_body); | ||||||
|     syn::custom_keyword!(path); |     syn::custom_keyword!(path); | ||||||
|     syn::custom_keyword!(query); |     syn::custom_keyword!(query); | ||||||
|     syn::custom_keyword!(query_map); |     syn::custom_keyword!(query_all); | ||||||
|     syn::custom_keyword!(header); |     syn::custom_keyword!(header); | ||||||
|     syn::custom_keyword!(error); |     syn::custom_keyword!(error); | ||||||
|     syn::custom_keyword!(manual_body_serde); |     syn::custom_keyword!(manual_body_serde); | ||||||
| @ -22,7 +22,7 @@ pub enum RequestMeta { | |||||||
|     RawBody, |     RawBody, | ||||||
|     Path, |     Path, | ||||||
|     Query, |     Query, | ||||||
|     QueryMap, |     QueryAll, | ||||||
|     Header(Ident), |     Header(Ident), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -41,9 +41,9 @@ impl Parse for RequestMeta { | |||||||
|         } else if lookahead.peek(kw::query) { |         } else if lookahead.peek(kw::query) { | ||||||
|             let _: kw::query = input.parse()?; |             let _: kw::query = input.parse()?; | ||||||
|             Ok(Self::Query) |             Ok(Self::Query) | ||||||
|         } else if lookahead.peek(kw::query_map) { |         } else if lookahead.peek(kw::query_all) { | ||||||
|             let _: kw::query_map = input.parse()?; |             let _: kw::query_all = input.parse()?; | ||||||
|             Ok(Self::QueryMap) |             Ok(Self::QueryAll) | ||||||
|         } else if lookahead.peek(kw::header) { |         } else if lookahead.peek(kw::header) { | ||||||
|             let _: kw::header = input.parse()?; |             let _: kw::header = input.parse()?; | ||||||
|             let _: Token![=] = input.parse()?; |             let _: Token![=] = input.parse()?; | ||||||
|  | |||||||
| @ -137,8 +137,8 @@ impl Request { | |||||||
|         self.fields.iter().find_map(RequestField::as_raw_body_field) |         self.fields.iter().find_map(RequestField::as_raw_body_field) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn query_map_field(&self) -> Option<&Field> { |     fn query_all_field(&self) -> Option<&Field> { | ||||||
|         self.fields.iter().find_map(RequestField::as_query_map_field) |         self.fields.iter().find_map(RequestField::as_query_all_field) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn expand_all(&self, ruma_common: &TokenStream) -> TokenStream { |     fn expand_all(&self, ruma_common: &TokenStream) -> TokenStream { | ||||||
| @ -161,7 +161,7 @@ impl Request { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         let request_query_def = if let Some(f) = self.query_map_field() { |         let request_query_def = if let Some(f) = self.query_all_field() { | ||||||
|             let field = Field { ident: None, colon_token: None, ..f.clone() }; |             let field = Field { ident: None, colon_token: None, ..f.clone() }; | ||||||
|             let field = PrivateField(&field); |             let field = PrivateField(&field); | ||||||
|             Some(quote! { (#field); }) |             Some(quote! { (#field); }) | ||||||
| @ -220,15 +220,15 @@ impl Request { | |||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let query_map_fields = |         let query_all_fields = | ||||||
|             self.fields.iter().filter(|f| matches!(&f.kind, RequestFieldKind::QueryMap)); |             self.fields.iter().filter(|f| matches!(&f.kind, RequestFieldKind::QueryAll)); | ||||||
|         let has_query_map_field = match query_map_fields.count() { |         let has_query_all_field = match query_all_fields.count() { | ||||||
|             0 => false, |             0 => false, | ||||||
|             1 => true, |             1 => true, | ||||||
|             _ => { |             _ => { | ||||||
|                 return Err(syn::Error::new_spanned( |                 return Err(syn::Error::new_spanned( | ||||||
|                     &self.ident, |                     &self.ident, | ||||||
|                     "Can't have more than one query_map field", |                     "Can't have more than one query_all field", | ||||||
|                 )) |                 )) | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
| @ -244,10 +244,10 @@ impl Request { | |||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if has_query_map_field && has_query_fields { |         if has_query_all_field && has_query_fields { | ||||||
|             return Err(syn::Error::new_spanned( |             return Err(syn::Error::new_spanned( | ||||||
|                 &self.ident, |                 &self.ident, | ||||||
|                 "Can't have both a query map field and regular query fields", |                 "Can't have both a query_all field and regular query fields", | ||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -307,8 +307,8 @@ pub(super) enum RequestFieldKind { | |||||||
|     /// Data that appears in the query string.
 |     /// Data that appears in the query string.
 | ||||||
|     Query, |     Query, | ||||||
| 
 | 
 | ||||||
|     /// Data that appears in the query string as dynamic key-value pairs.
 |     /// Data that represents all the query string as a single type.
 | ||||||
|     QueryMap, |     QueryAll, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl RequestField { | impl RequestField { | ||||||
| @ -319,7 +319,7 @@ impl RequestField { | |||||||
|             Some(RequestMeta::RawBody) => RequestFieldKind::RawBody, |             Some(RequestMeta::RawBody) => RequestFieldKind::RawBody, | ||||||
|             Some(RequestMeta::Path) => RequestFieldKind::Path, |             Some(RequestMeta::Path) => RequestFieldKind::Path, | ||||||
|             Some(RequestMeta::Query) => RequestFieldKind::Query, |             Some(RequestMeta::Query) => RequestFieldKind::Query, | ||||||
|             Some(RequestMeta::QueryMap) => RequestFieldKind::QueryMap, |             Some(RequestMeta::QueryAll) => RequestFieldKind::QueryAll, | ||||||
|             Some(RequestMeta::Header(header)) => RequestFieldKind::Header(header), |             Some(RequestMeta::Header(header)) => RequestFieldKind::Header(header), | ||||||
|             None => RequestFieldKind::Body, |             None => RequestFieldKind::Body, | ||||||
|         }; |         }; | ||||||
| @ -359,10 +359,10 @@ impl RequestField { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Return the contained field if this request field is a query map kind.
 |     /// Return the contained field if this request field is a query all kind.
 | ||||||
|     pub fn as_query_map_field(&self) -> Option<&Field> { |     pub fn as_query_all_field(&self) -> Option<&Field> { | ||||||
|         match &self.kind { |         match &self.kind { | ||||||
|             RequestFieldKind::QueryMap => Some(&self.inner), |             RequestFieldKind::QueryAll => Some(&self.inner), | ||||||
|             _ => None, |             _ => None, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ impl Request { | |||||||
|             (TokenStream::new(), TokenStream::new()) |             (TokenStream::new(), TokenStream::new()) | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let (parse_query, query_vars) = if let Some(field) = self.query_map_field() { |         let (parse_query, query_vars) = if let Some(field) = self.query_all_field() { | ||||||
|             let cfg_attrs = |             let cfg_attrs = | ||||||
|                 field.attrs.iter().filter(|a| a.path().is_ident("cfg")).collect::<Vec<_>>(); |                 field.attrs.iter().filter(|a| a.path().is_ident("cfg")).collect::<Vec<_>>(); | ||||||
|             let field_name = field.ident.as_ref().expect("expected field to have an identifier"); |             let field_name = field.ident.as_ref().expect("expected field to have an identifier"); | ||||||
|  | |||||||
| @ -15,29 +15,11 @@ impl Request { | |||||||
|         let path_fields = |         let path_fields = | ||||||
|             self.path_fields().map(|f| f.ident.as_ref().expect("path fields have a name")); |             self.path_fields().map(|f| f.ident.as_ref().expect("path fields have a name")); | ||||||
| 
 | 
 | ||||||
|         let request_query_string = if let Some(field) = self.query_map_field() { |         let request_query_string = if let Some(field) = self.query_all_field() { | ||||||
|             let field_name = field.ident.as_ref().expect("expected field to have identifier"); |             let field_name = field.ident.as_ref().expect("expected field to have identifier"); | ||||||
| 
 | 
 | ||||||
|             quote! {{ |             quote! {{ | ||||||
|                 // This function exists so that the compiler will throw an error when the type of
 |  | ||||||
|                 // the field with the query_map attribute doesn't implement
 |  | ||||||
|                 // `IntoIterator<Item = (String, String)>`.
 |  | ||||||
|                 //
 |  | ||||||
|                 // This is necessary because the `serde_html_form::to_string` call will result in a
 |  | ||||||
|                 // runtime error when the type cannot be encoded as a list key-value pairs
 |  | ||||||
|                 // (?key1=value1&key2=value2).
 |  | ||||||
|                 //
 |  | ||||||
|                 // By asserting that it implements the iterator trait, we can ensure that it won't
 |  | ||||||
|                 // fail.
 |  | ||||||
|                 fn assert_trait_impl<T>(_: &T) |  | ||||||
|                 where |  | ||||||
|                     T: ::std::iter::IntoIterator< |  | ||||||
|                         Item = (::std::string::String, ::std::string::String), |  | ||||||
|                     >, |  | ||||||
|                 {} |  | ||||||
| 
 |  | ||||||
|                 let request_query = RequestQuery(self.#field_name); |                 let request_query = RequestQuery(self.#field_name); | ||||||
|                 assert_trait_impl(&request_query.0); |  | ||||||
| 
 | 
 | ||||||
|                 &#serde_html_form::to_string(request_query)? |                 &#serde_html_form::to_string(request_query)? | ||||||
|             }} |             }} | ||||||
|  | |||||||
| @ -170,7 +170,17 @@ fn expand_deserialize_impl( | |||||||
|             }; |             }; | ||||||
|             let self_variant = variant.ctor(quote! { Self }); |             let self_variant = variant.ctor(quote! { Self }); | ||||||
|             let content = event.to_event_path(kind, var); |             let content = event.to_event_path(kind, var); | ||||||
|             let ev_types = event.aliases.iter().chain([&event.ev_type]); |             let ev_types = event.aliases.iter().chain([&event.ev_type]).map(|ev_type| { | ||||||
|  |                 if event.has_type_fragment() { | ||||||
|  |                     let ev_type = ev_type.value(); | ||||||
|  |                     let prefix = ev_type | ||||||
|  |                         .strip_suffix('*') | ||||||
|  |                         .expect("event type with type fragment must end with *"); | ||||||
|  |                     quote! { t if t.starts_with(#prefix) } | ||||||
|  |                 } else { | ||||||
|  |                     quote! { #ev_type } | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
| 
 | 
 | ||||||
|             Ok(quote! { |             Ok(quote! { | ||||||
|                 #variant_attrs #(#ev_types)|* => { |                 #variant_attrs #(#ev_types)|* => { | ||||||
| @ -328,6 +338,55 @@ fn expand_content_enum( | |||||||
|     let serialize_custom_event_error_path = |     let serialize_custom_event_error_path = | ||||||
|         quote! { #ruma_events::serialize_custom_event_error }.to_string(); |         quote! { #ruma_events::serialize_custom_event_error }.to_string(); | ||||||
| 
 | 
 | ||||||
|  |     // Generate an `EventContentFromType` implementation.
 | ||||||
|  |     let serde_json = quote! { #ruma_events::exports::serde_json }; | ||||||
|  |     let event_type_match_arms: TokenStream = events | ||||||
|  |         .iter() | ||||||
|  |         .map(|event| { | ||||||
|  |             let variant = event.to_variant()?; | ||||||
|  |             let variant_attrs = { | ||||||
|  |                 let attrs = &variant.attrs; | ||||||
|  |                 quote! { #(#attrs)* } | ||||||
|  |             }; | ||||||
|  |             let self_variant = variant.ctor(quote! { Self }); | ||||||
|  | 
 | ||||||
|  |             let ev_types = event.aliases.iter().chain([&event.ev_type]).map(|ev_type| { | ||||||
|  |                 if event.has_type_fragment() { | ||||||
|  |                     let ev_type = ev_type.value(); | ||||||
|  |                     let prefix = ev_type | ||||||
|  |                         .strip_suffix('*') | ||||||
|  |                         .expect("event type with type fragment must end with *"); | ||||||
|  |                     quote! { t if t.starts_with(#prefix) } | ||||||
|  |                 } else { | ||||||
|  |                     quote! { #ev_type } | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             let deserialize_content = if event.has_type_fragment() { | ||||||
|  |                 // If the event has a type fragment, then it implements EventContentFromType itself;
 | ||||||
|  |                 // see `generate_event_content_impl` which does that. In this case, forward to its
 | ||||||
|  |                 // implementation.
 | ||||||
|  |                 let content_type = event.to_event_content_path(kind, None); | ||||||
|  |                 quote! { | ||||||
|  |                     #content_type::from_parts(event_type, json)? | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 // The event doesn't have a type fragment, so it *should* implement Deserialize:
 | ||||||
|  |                 // use that here.
 | ||||||
|  |                 quote! { | ||||||
|  |                     #serde_json::from_str(json.get())? | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             Ok(quote! { | ||||||
|  |                 #variant_attrs #(#ev_types)|* => { | ||||||
|  |                     let content = #deserialize_content; | ||||||
|  |                     Ok(#self_variant(content)) | ||||||
|  |                 }, | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |         .collect::<syn::Result<_>>()?; | ||||||
|  | 
 | ||||||
|     Ok(quote! { |     Ok(quote! { | ||||||
|         #( #attrs )* |         #( #attrs )* | ||||||
|         #[derive(Clone, Debug, #serde::Serialize)] |         #[derive(Clone, Debug, #serde::Serialize)] | ||||||
| @ -358,6 +417,23 @@ fn expand_content_enum( | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         #[automatically_derived] | ||||||
|  |         impl #ruma_events::EventContentFromType for #ident { | ||||||
|  |             fn from_parts(event_type: &str, json: &#serde_json::value::RawValue) -> serde_json::Result<Self> { | ||||||
|  |                 match event_type { | ||||||
|  |                     #event_type_match_arms | ||||||
|  | 
 | ||||||
|  |                     _ => { | ||||||
|  |                         Ok(Self::_Custom { | ||||||
|  |                             event_type: crate::PrivOwnedStr( | ||||||
|  |                                 ::std::convert::From::from(event_type.to_owned()) | ||||||
|  |                             ) | ||||||
|  |                         }) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         #[automatically_derived] |         #[automatically_derived] | ||||||
|         impl #ruma_events::#sub_trait_name for #ident { |         impl #ruma_events::#sub_trait_name for #ident { | ||||||
|             #state_event_content_impl |             #state_event_content_impl | ||||||
|  | |||||||
| @ -4,6 +4,19 @@ Breaking changes: | |||||||
| 
 | 
 | ||||||
| - The `XMatrix::new` method now takes `OwnedServerName` instead of `Option<OwnedServerName>` | - The `XMatrix::new` method now takes `OwnedServerName` instead of `Option<OwnedServerName>` | ||||||
|   for the destination, since servers must always set the destination. |   for the destination, since servers must always set the destination. | ||||||
|  | - The `sig` field in `XMatrix` has been changed from `String` to `Base64` to more accurately | ||||||
|  |   mirror its allowed values in the type system. | ||||||
|  | 
 | ||||||
|  | Bug fixes: | ||||||
|  | 
 | ||||||
|  | - When encoding to a header value, `XMatrix` fields are now quoted and escaped correctly. | ||||||
|  | - Use http-auth crate to parse `XMatrix`. Allows to parse the Authorization HTTP | ||||||
|  |   header with full compatibility with RFC 7235 | ||||||
|  | 
 | ||||||
|  | Improvements: | ||||||
|  | 
 | ||||||
|  | - Implement `Display`, `FromStr` and conversion to/from `http::HeaderValue` for | ||||||
|  |   `XMatrix` | ||||||
| 
 | 
 | ||||||
| # 0.3.0 | # 0.3.0 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,9 +16,11 @@ all-features = true | |||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| headers = "0.4.0" | headers = "0.4.0" | ||||||
|  | http = { workspace = true } | ||||||
|  | http-auth = { version = "0.1.9", default-features = false } | ||||||
| ruma-common = { workspace = true } | ruma-common = { workspace = true } | ||||||
|  | thiserror = { workspace = true } | ||||||
| tracing = { workspace = true } | tracing = { workspace = true } | ||||||
| yap = "0.12.0" |  | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| tracing-subscriber = "0.3.16" | tracing-subscriber = "0.3.16" | ||||||
|  | |||||||
| @ -1,30 +1,38 @@ | |||||||
| //! Common types for implementing federation authorization.
 | //! Common types for implementing federation authorization.
 | ||||||
| 
 | 
 | ||||||
| use headers::{authorization::Credentials, HeaderValue}; | use std::{borrow::Cow, fmt, str::FromStr}; | ||||||
| use ruma_common::{OwnedServerName, OwnedServerSigningKeyId}; | 
 | ||||||
|  | use headers::authorization::Credentials; | ||||||
|  | use http::HeaderValue; | ||||||
|  | use http_auth::ChallengeParser; | ||||||
|  | use ruma_common::{ | ||||||
|  |     serde::{Base64, Base64DecodeError}, | ||||||
|  |     IdParseError, OwnedServerName, OwnedServerSigningKeyId, | ||||||
|  | }; | ||||||
|  | use thiserror::Error; | ||||||
| use tracing::debug; | use tracing::debug; | ||||||
| use yap::{IntoTokens, TokenLocation, Tokens}; |  | ||||||
| 
 | 
 | ||||||
| /// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
 | /// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
 | ||||||
| /// [Matrix Server-Server API][spec]. Includes an implementation of
 | /// [Matrix Server-Server API][spec].
 | ||||||
| /// [`headers::authorization::Credentials`] for automatically handling the encoding and decoding
 |  | ||||||
| /// when using a web framework that supports typed headers.
 |  | ||||||
| ///
 | ///
 | ||||||
| /// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
 | /// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
 | ||||||
|  | #[derive(Clone)] | ||||||
| #[non_exhaustive] | #[non_exhaustive] | ||||||
| pub struct XMatrix { | pub struct XMatrix { | ||||||
|     /// The server name of the sending server.
 |     /// The server name of the sending server.
 | ||||||
|     pub origin: OwnedServerName, |     pub origin: OwnedServerName, | ||||||
|     /// The server name of the receiving sender. For compatibility with older servers, recipients
 |     /// The server name of the receiving sender.
 | ||||||
|     /// should accept requests without this parameter, but MUST always send it. If this property is
 |     ///
 | ||||||
|     /// included, but the value does not match the receiving server's name, the receiving server
 |     /// For compatibility with older servers, recipients should accept requests without this
 | ||||||
|     /// must deny the request with an HTTP status code 401 Unauthorized.
 |     /// parameter, but MUST always send it. If this property is included, but the value does
 | ||||||
|  |     /// not match the receiving server's name, the receiving server must deny the request with
 | ||||||
|  |     /// an HTTP status code 401 Unauthorized.
 | ||||||
|     pub destination: Option<OwnedServerName>, |     pub destination: Option<OwnedServerName>, | ||||||
|     /// The ID - including the algorithm name - of the sending server's key that was used to sign
 |     /// The ID - including the algorithm name - of the sending server's key that was used to sign
 | ||||||
|     /// the request.
 |     /// the request.
 | ||||||
|     pub key: OwnedServerSigningKeyId, |     pub key: OwnedServerSigningKeyId, | ||||||
|     /// The signature of the JSON.
 |     /// The signature of the JSON.
 | ||||||
|     pub sig: String, |     pub sig: Base64, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl XMatrix { | impl XMatrix { | ||||||
| @ -33,227 +41,214 @@ impl XMatrix { | |||||||
|         origin: OwnedServerName, |         origin: OwnedServerName, | ||||||
|         destination: OwnedServerName, |         destination: OwnedServerName, | ||||||
|         key: OwnedServerSigningKeyId, |         key: OwnedServerSigningKeyId, | ||||||
|         sig: String, |         sig: Base64, | ||||||
|     ) -> Self { |     ) -> Self { | ||||||
|         Self { origin, destination: Some(destination), key, sig } |         Self { origin, destination: Some(destination), key, sig } | ||||||
|     } |     } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| fn parse_token<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> { |     /// Parse an X-Matrix Authorization header from the given string.
 | ||||||
|     tokens.optional(|t| { |     pub fn parse(s: impl AsRef<str>) -> Result<Self, XMatrixParseError> { | ||||||
|         let token: Vec<u8> = t.take_while(|c| is_tchar(**c)).as_iter().copied().collect(); |         let parser = ChallengeParser::new(s.as_ref()); | ||||||
|         if !token.is_empty() { |         let mut xmatrix = None; | ||||||
|             Some(token) |  | ||||||
|         } else { |  | ||||||
|             debug!("Returning early because of empty token at {}", t.location().offset()); |  | ||||||
|             None |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| fn parse_token_with_colons<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> { |         for challenge in parser { | ||||||
|     tokens.optional(|t| { |             let challenge = challenge?; | ||||||
|         let token: Vec<u8> = |  | ||||||
|             t.take_while(|c| is_tchar(**c) || **c == b':').as_iter().copied().collect(); |  | ||||||
|         if !token.is_empty() { |  | ||||||
|             Some(token) |  | ||||||
|         } else { |  | ||||||
|             debug!("Returning early because of empty token at {}", t.location().offset()); |  | ||||||
|             None |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| fn parse_quoted<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> { |             if challenge.scheme.eq_ignore_ascii_case(XMatrix::SCHEME) { | ||||||
|     tokens.optional(|t| { |                 xmatrix = Some(challenge); | ||||||
|         if !(t.token(&b'"')) { |                 break; | ||||||
|             return None; |  | ||||||
|         } |  | ||||||
|         let mut buffer = Vec::new(); |  | ||||||
|         loop { |  | ||||||
|             match t.next()? { |  | ||||||
|                 // quoted pair
 |  | ||||||
|                 b'\\' => { |  | ||||||
|                     let escaped = t.next().filter(|c| { |  | ||||||
|                         if is_quoted_pair(**c) { |  | ||||||
|                             true |  | ||||||
|                         } else { |  | ||||||
|                             debug!( |  | ||||||
|                                 "Encountered an illegal character {} at location {}", |  | ||||||
|                                 **c as char, |  | ||||||
|                                 t.location().offset() |  | ||||||
|                             ); |  | ||||||
|                             false |  | ||||||
|                         } |  | ||||||
|                     })?; |  | ||||||
|                     buffer.push(*escaped); |  | ||||||
|                 } |  | ||||||
|                 // end of quote
 |  | ||||||
|                 b'"' => break, |  | ||||||
|                 // regular character
 |  | ||||||
|                 c if is_qdtext(*c) => buffer.push(*c), |  | ||||||
|                 // Invalid character
 |  | ||||||
|                 c => { |  | ||||||
|                     debug!( |  | ||||||
|                         "Encountered an illegal character {} at location {}", |  | ||||||
|                         *c as char, |  | ||||||
|                         t.location().offset() |  | ||||||
|                     ); |  | ||||||
|                     return None; |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         Some(buffer) |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| fn parse_xmatrix_field<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<(String, Vec<u8>)> { |         let Some(xmatrix) = xmatrix else { | ||||||
|     tokens.optional(|t| { |             return Err(XMatrixParseError::NotFound); | ||||||
|         let name = parse_token(t).and_then(|name| { |         }; | ||||||
|             let name = std::str::from_utf8(&name).ok()?.to_ascii_lowercase(); |  | ||||||
|             match name.as_str() { |  | ||||||
|                 "origin" | "destination" | "key" | "sig" => Some(name), |  | ||||||
|                 name => { |  | ||||||
|                     debug!( |  | ||||||
|                         "Encountered an invalid field name {} at location {}", |  | ||||||
|                         name, |  | ||||||
|                         t.location().offset() |  | ||||||
|                     ); |  | ||||||
|                     None |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         })?; |  | ||||||
| 
 | 
 | ||||||
|         if !t.token(&b'=') { |  | ||||||
|             return None; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let value = parse_quoted(t).or_else(|| parse_token_with_colons(t))?; |  | ||||||
| 
 |  | ||||||
|         Some((name, value)) |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn parse_xmatrix<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<XMatrix> { |  | ||||||
|     tokens.optional(|t| { |  | ||||||
|         if !t.tokens(b"X-Matrix ") { |  | ||||||
|             debug!("Failed to parse X-Matrix credentials, didn't start with 'X-Matrix '"); |  | ||||||
|             return None; |  | ||||||
|         } |  | ||||||
|         let mut origin = None; |         let mut origin = None; | ||||||
|         let mut destination = None; |         let mut destination = None; | ||||||
|         let mut key = None; |         let mut key = None; | ||||||
|         let mut sig = None; |         let mut sig = None; | ||||||
| 
 | 
 | ||||||
|         for (name, value) in t.sep_by(|t| parse_xmatrix_field(t), |t| t.token(&b',')).as_iter() { |         for (name, value) in xmatrix.params { | ||||||
|             match name.as_str() { |             if name.eq_ignore_ascii_case("origin") { | ||||||
|                 "origin" => { |                 if origin.is_some() { | ||||||
|                     if origin.is_some() { |                     return Err(XMatrixParseError::DuplicateParameter("origin".to_owned())); | ||||||
|                         debug!("Field origin duplicated in X-Matrix Authorization header"); |                 } else { | ||||||
|                     } |                     origin = Some(OwnedServerName::try_from(value.to_unescaped())?); | ||||||
|                     origin = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?); |  | ||||||
|                 } |                 } | ||||||
|                 "destination" => { |             } else if name.eq_ignore_ascii_case("destination") { | ||||||
|                     if destination.is_some() { |                 if destination.is_some() { | ||||||
|                         debug!("Field destination duplicated in X-Matrix Authorization header"); |                     return Err(XMatrixParseError::DuplicateParameter("destination".to_owned())); | ||||||
|                     } |                 } else { | ||||||
|                     destination = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?); |                     destination = Some(OwnedServerName::try_from(value.to_unescaped())?); | ||||||
|                 } |                 } | ||||||
|                 "key" => { |             } else if name.eq_ignore_ascii_case("key") { | ||||||
|                     if key.is_some() { |                 if key.is_some() { | ||||||
|                         debug!("Field key duplicated in X-Matrix Authorization header"); |                     return Err(XMatrixParseError::DuplicateParameter("key".to_owned())); | ||||||
|                     } |                 } else { | ||||||
|                     key = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?); |                     key = Some(OwnedServerSigningKeyId::try_from(value.to_unescaped())?); | ||||||
|                 } |                 } | ||||||
|                 "sig" => { |             } else if name.eq_ignore_ascii_case("sig") { | ||||||
|                     if sig.is_some() { |                 if sig.is_some() { | ||||||
|                         debug!("Field sig duplicated in X-Matrix Authorization header"); |                     return Err(XMatrixParseError::DuplicateParameter("sig".to_owned())); | ||||||
|                     } |                 } else { | ||||||
|                     sig = Some(std::str::from_utf8(&value).ok()?.to_owned()); |                     sig = Some(Base64::parse(value.to_unescaped())?); | ||||||
|                 } |  | ||||||
|                 name => { |  | ||||||
|                     debug!("Unknown field {} found in X-Matrix Authorization header", name); |  | ||||||
|                 } |                 } | ||||||
|  |             } else { | ||||||
|  |                 debug!("Unknown parameter {name} in X-Matrix Authorization header"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Some(XMatrix { origin: origin?, destination, key: key?, sig: sig? }) |         Ok(Self { | ||||||
|     }) |             origin: origin | ||||||
|  |                 .ok_or_else(|| XMatrixParseError::MissingParameter("origin".to_owned()))?, | ||||||
|  |             destination, | ||||||
|  |             key: key.ok_or_else(|| XMatrixParseError::MissingParameter("key".to_owned()))?, | ||||||
|  |             sig: sig.ok_or_else(|| XMatrixParseError::MissingParameter("sig".to_owned()))?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_alpha(c: u8) -> bool { | impl fmt::Debug for XMatrix { | ||||||
|     (0x41..=0x5A).contains(&c) || (0x61..=0x7A).contains(&c) |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|  |         f.debug_struct("XMatrix") | ||||||
|  |             .field("origin", &self.origin) | ||||||
|  |             .field("destination", &self.destination) | ||||||
|  |             .field("key", &self.key) | ||||||
|  |             .finish_non_exhaustive() | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_digit(c: u8) -> bool { | /// Whether the given char is a [token char].
 | ||||||
|     (0x30..=0x39).contains(&c) | ///
 | ||||||
|  | /// [token char]: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2
 | ||||||
|  | fn is_tchar(c: char) -> bool { | ||||||
|  |     const TOKEN_CHARS: [char; 15] = | ||||||
|  |         ['!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~']; | ||||||
|  |     c.is_ascii_alphanumeric() || TOKEN_CHARS.contains(&c) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_tchar(c: u8) -> bool { | /// If the field value does not contain only token chars, convert it to a [quoted string].
 | ||||||
|     const TOKEN_CHARS: [u8; 15] = | ///
 | ||||||
|         [b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'.', b'^', b'_', b'`', b'|', b'~']; | /// [quoted string]: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4
 | ||||||
|     is_alpha(c) || is_digit(c) || TOKEN_CHARS.contains(&c) | fn escape_field_value(value: &str) -> Cow<'_, str> { | ||||||
|  |     if !value.is_empty() && value.chars().all(is_tchar) { | ||||||
|  |         return Cow::Borrowed(value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let value = value.replace('\\', r#"\\"#).replace('"', r#"\""#); | ||||||
|  |     Cow::Owned(format!("\"{value}\"")) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_qdtext(c: u8) -> bool { | impl fmt::Display for XMatrix { | ||||||
|     c == b'\t' |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|         || c == b' ' |         let Self { origin, destination, key, sig } = self; | ||||||
|         || c == 0x21 | 
 | ||||||
|         || (0x23..=0x5B).contains(&c) |         let origin = escape_field_value(origin.as_str()); | ||||||
|         || (0x5D..=0x7E).contains(&c) |         let key = escape_field_value(key.as_str()); | ||||||
|         || is_obs_text(c) |         let sig = sig.encode(); | ||||||
|  |         let sig = escape_field_value(&sig); | ||||||
|  | 
 | ||||||
|  |         write!(f, r#"{} "#, Self::SCHEME)?; | ||||||
|  | 
 | ||||||
|  |         if let Some(destination) = destination { | ||||||
|  |             let destination = escape_field_value(destination.as_str()); | ||||||
|  |             write!(f, r#"destination={destination},"#)?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         write!(f, "key={key},origin={origin},sig={sig}") | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_obs_text(c: u8) -> bool { | impl FromStr for XMatrix { | ||||||
|     c >= 0x80 // The spec does contain an upper limit of 0xFF here, but that's enforced by the type
 |     type Err = XMatrixParseError; | ||||||
|  | 
 | ||||||
|  |     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||||
|  |         Self::parse(s) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_vchar(c: u8) -> bool { | impl TryFrom<&HeaderValue> for XMatrix { | ||||||
|     (0x21..=0x7E).contains(&c) |     type Error = XMatrixParseError; | ||||||
|  | 
 | ||||||
|  |     fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> { | ||||||
|  |         Self::parse(value.to_str()?) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn is_quoted_pair(c: u8) -> bool { | impl From<&XMatrix> for HeaderValue { | ||||||
|     c == b'\t' || c == b' ' || is_vchar(c) || is_obs_text(c) |     fn from(value: &XMatrix) -> Self { | ||||||
|  |         value.to_string().try_into().expect("header format is static") | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Credentials for XMatrix { | impl Credentials for XMatrix { | ||||||
|     const SCHEME: &'static str = "X-Matrix"; |     const SCHEME: &'static str = "X-Matrix"; | ||||||
| 
 | 
 | ||||||
|     fn decode(value: &HeaderValue) -> Option<Self> { |     fn decode(value: &HeaderValue) -> Option<Self> { | ||||||
|         let value: Vec<u8> = value.as_bytes().to_vec(); |         value.try_into().ok() | ||||||
|         parse_xmatrix(&mut value.into_tokens()) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn encode(&self) -> HeaderValue { |     fn encode(&self) -> HeaderValue { | ||||||
|         if let Some(destination) = &self.destination { |         self.into() | ||||||
|             format!( |     } | ||||||
|                 "X-Matrix origin=\"{}\",destination=\"{destination}\",key=\"{}\",sig=\"{}\"", | } | ||||||
|                 self.origin, self.key, self.sig | 
 | ||||||
|             ) | /// An error when trying to parse an X-Matrix Authorization header.
 | ||||||
|         } else { | #[derive(Debug, Error)] | ||||||
|             format!("X-Matrix origin=\"{}\",key=\"{}\",sig=\"{}\"", self.origin, self.key, self.sig) | #[non_exhaustive] | ||||||
|         } | pub enum XMatrixParseError { | ||||||
|         .try_into() |     /// The `HeaderValue` could not be converted to a `str`.
 | ||||||
|         .expect("header format is static") |     #[error(transparent)] | ||||||
|  |     ToStr(#[from] http::header::ToStrError), | ||||||
|  | 
 | ||||||
|  |     /// The string could not be parsed as a valid Authorization string.
 | ||||||
|  |     #[error("{0}")] | ||||||
|  |     ParseStr(String), | ||||||
|  | 
 | ||||||
|  |     /// The credentials with the X-Matrix scheme were not found.
 | ||||||
|  |     #[error("X-Matrix credentials not found")] | ||||||
|  |     NotFound, | ||||||
|  | 
 | ||||||
|  |     /// The parameter value could not be parsed as a Matrix ID.
 | ||||||
|  |     #[error(transparent)] | ||||||
|  |     ParseId(#[from] IdParseError), | ||||||
|  | 
 | ||||||
|  |     /// The parameter value could not be parsed as base64.
 | ||||||
|  |     #[error(transparent)] | ||||||
|  |     ParseBase64(#[from] Base64DecodeError), | ||||||
|  | 
 | ||||||
|  |     /// The parameter with the given name was not found.
 | ||||||
|  |     #[error("missing parameter '{0}'")] | ||||||
|  |     MissingParameter(String), | ||||||
|  | 
 | ||||||
|  |     /// The parameter with the given name was found more than once.
 | ||||||
|  |     #[error("duplicate parameter '{0}'")] | ||||||
|  |     DuplicateParameter(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> From<http_auth::parser::Error<'a>> for XMatrixParseError { | ||||||
|  |     fn from(value: http_auth::parser::Error<'a>) -> Self { | ||||||
|  |         Self::ParseStr(value.to_string()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use headers::{authorization::Credentials, HeaderValue}; |     use headers::{authorization::Credentials, HeaderValue}; | ||||||
|     use ruma_common::OwnedServerName; |     use ruma_common::{serde::Base64, OwnedServerName}; | ||||||
| 
 | 
 | ||||||
|     use super::XMatrix; |     use super::XMatrix; | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn xmatrix_auth_pre_1_3() { |     fn xmatrix_auth_pre_1_3() { | ||||||
|         let header = HeaderValue::from_static( |         let header = HeaderValue::from_static( | ||||||
|             "X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"ABCDEF...\"", |             "X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"", | ||||||
|         ); |         ); | ||||||
|         let origin = "origin.hs.example.com".try_into().unwrap(); |         let origin = "origin.hs.example.com".try_into().unwrap(); | ||||||
|         let key = "ed25519:key1".try_into().unwrap(); |         let key = "ed25519:key1".try_into().unwrap(); | ||||||
|         let sig = "ABCDEF...".to_owned(); |         let sig = Base64::new(b"test".to_vec()); | ||||||
|         let credentials: XMatrix = Credentials::decode(&header).unwrap(); |         let credentials = XMatrix::try_from(&header).unwrap(); | ||||||
|         assert_eq!(credentials.origin, origin); |         assert_eq!(credentials.origin, origin); | ||||||
|         assert_eq!(credentials.destination, None); |         assert_eq!(credentials.destination, None); | ||||||
|         assert_eq!(credentials.key, key); |         assert_eq!(credentials.key, key); | ||||||
| @ -261,17 +256,20 @@ mod tests { | |||||||
| 
 | 
 | ||||||
|         let credentials = XMatrix { origin, destination: None, key, sig }; |         let credentials = XMatrix { origin, destination: None, key, sig }; | ||||||
| 
 | 
 | ||||||
|         assert_eq!(credentials.encode(), header); |         assert_eq!( | ||||||
|  |             credentials.encode(), | ||||||
|  |             "X-Matrix key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA" | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn xmatrix_auth_1_3() { |     fn xmatrix_auth_1_3() { | ||||||
|         let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"ABCDEF...\""); |         let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\""); | ||||||
|         let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap(); |         let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap(); | ||||||
|         let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap(); |         let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap(); | ||||||
|         let key = "ed25519:key1".try_into().unwrap(); |         let key = "ed25519:key1".try_into().unwrap(); | ||||||
|         let sig = "ABCDEF...".to_owned(); |         let sig = Base64::new(b"test".to_vec()); | ||||||
|         let credentials: XMatrix = Credentials::decode(&header).unwrap(); |         let credentials = XMatrix::try_from(&header).unwrap(); | ||||||
|         assert_eq!(credentials.origin, origin); |         assert_eq!(credentials.origin, origin); | ||||||
|         assert_eq!(credentials.destination, Some(destination.clone())); |         assert_eq!(credentials.destination, Some(destination.clone())); | ||||||
|         assert_eq!(credentials.key, key); |         assert_eq!(credentials.key, key); | ||||||
| @ -279,6 +277,41 @@ mod tests { | |||||||
| 
 | 
 | ||||||
|         let credentials = XMatrix::new(origin, destination, key, sig); |         let credentials = XMatrix::new(origin, destination, key, sig); | ||||||
| 
 | 
 | ||||||
|         assert_eq!(credentials.encode(), header); |         assert_eq!(credentials.encode(), "X-Matrix destination=destination.hs.example.com,key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn xmatrix_quoting() { | ||||||
|  |         let header = HeaderValue::from_static( | ||||||
|  |             r#"X-Matrix origin="example.com:1234",key="abc\"def\\:ghi",sig=dGVzdA,"#, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let origin: OwnedServerName = "example.com:1234".try_into().unwrap(); | ||||||
|  |         let key = r#"abc"def\:ghi"#.try_into().unwrap(); | ||||||
|  |         let sig = Base64::new(b"test".to_vec()); | ||||||
|  |         let credentials = XMatrix::try_from(&header).unwrap(); | ||||||
|  |         assert_eq!(credentials.origin, origin); | ||||||
|  |         assert_eq!(credentials.destination, None); | ||||||
|  |         assert_eq!(credentials.key, key); | ||||||
|  |         assert_eq!(credentials.sig, sig); | ||||||
|  | 
 | ||||||
|  |         let credentials = XMatrix { origin, destination: None, key, sig }; | ||||||
|  | 
 | ||||||
|  |         assert_eq!( | ||||||
|  |             credentials.encode(), | ||||||
|  |             r#"X-Matrix key="abc\"def\\:ghi",origin="example.com:1234",sig=dGVzdA"# | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn xmatrix_auth_1_3_with_extra_spaces() { | ||||||
|  |         let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\"  ,     destination=\"destination.hs.example.com\",key=\"ed25519:key1\", sig=\"dGVzdA\""); | ||||||
|  |         let credentials = XMatrix::try_from(&header).unwrap(); | ||||||
|  |         let sig = Base64::new(b"test".to_vec()); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(credentials.origin, "origin.hs.example.com"); | ||||||
|  |         assert_eq!(credentials.destination.unwrap(), "destination.hs.example.com"); | ||||||
|  |         assert_eq!(credentials.key, "ed25519:key1"); | ||||||
|  |         assert_eq!(credentials.sig, sig); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -226,7 +226,6 @@ unstable-msc2448 = [ | |||||||
| ] | ] | ||||||
| unstable-msc2654 = ["ruma-client-api?/unstable-msc2654"] | unstable-msc2654 = ["ruma-client-api?/unstable-msc2654"] | ||||||
| unstable-msc2666 = ["ruma-client-api?/unstable-msc2666"] | unstable-msc2666 = ["ruma-client-api?/unstable-msc2666"] | ||||||
| unstable-msc2705 = ["ruma-client-api?/unstable-msc2705"] |  | ||||||
| unstable-msc2747 = ["ruma-events?/unstable-msc2747"] | unstable-msc2747 = ["ruma-events?/unstable-msc2747"] | ||||||
| unstable-msc2867 = ["ruma-events?/unstable-msc2867"] | unstable-msc2867 = ["ruma-events?/unstable-msc2867"] | ||||||
| unstable-msc2870 = ["ruma-common/unstable-msc2870"] | unstable-msc2870 = ["ruma-common/unstable-msc2870"] | ||||||
| @ -242,10 +241,10 @@ unstable-msc3245 = ["ruma-events?/unstable-msc3245"] | |||||||
| unstable-msc3245-v1-compat = ["ruma-events?/unstable-msc3245-v1-compat"] | unstable-msc3245-v1-compat = ["ruma-events?/unstable-msc3245-v1-compat"] | ||||||
| unstable-msc3246 = ["ruma-events?/unstable-msc3246"] | unstable-msc3246 = ["ruma-events?/unstable-msc3246"] | ||||||
| unstable-msc3266 = ["ruma-client-api?/unstable-msc3266"] | unstable-msc3266 = ["ruma-client-api?/unstable-msc3266"] | ||||||
| unstable-msc3291 = ["ruma-events?/unstable-msc3291"] |  | ||||||
| unstable-msc3381 = ["ruma-events?/unstable-msc3381"] | unstable-msc3381 = ["ruma-events?/unstable-msc3381"] | ||||||
| unstable-msc3401 = ["ruma-events?/unstable-msc3401"] | unstable-msc3401 = ["ruma-events?/unstable-msc3401"] | ||||||
| unstable-msc3488 = ["ruma-client-api?/unstable-msc3488", "ruma-events?/unstable-msc3488"] | unstable-msc3488 = ["ruma-client-api?/unstable-msc3488", "ruma-events?/unstable-msc3488"] | ||||||
|  | unstable-msc3489 = ["ruma-events?/unstable-msc3489"] | ||||||
| unstable-msc3551 = ["ruma-events?/unstable-msc3551"] | unstable-msc3551 = ["ruma-events?/unstable-msc3551"] | ||||||
| unstable-msc3552 = ["ruma-events?/unstable-msc3552"] | unstable-msc3552 = ["ruma-events?/unstable-msc3552"] | ||||||
| unstable-msc3553 = ["ruma-events?/unstable-msc3553"] | unstable-msc3553 = ["ruma-events?/unstable-msc3553"] | ||||||
| @ -255,7 +254,6 @@ unstable-msc3618 = ["ruma-federation-api?/unstable-msc3618"] | |||||||
| unstable-msc3723 = ["ruma-federation-api?/unstable-msc3723"] | unstable-msc3723 = ["ruma-federation-api?/unstable-msc3723"] | ||||||
| unstable-msc3814 = ["ruma-client-api?/unstable-msc3814"] | unstable-msc3814 = ["ruma-client-api?/unstable-msc3814"] | ||||||
| unstable-msc3843 = ["ruma-client-api?/unstable-msc3843", "ruma-federation-api?/unstable-msc3843"] | unstable-msc3843 = ["ruma-client-api?/unstable-msc3843", "ruma-federation-api?/unstable-msc3843"] | ||||||
| unstable-msc3916 = ["ruma-client-api?/unstable-msc3916"] |  | ||||||
| unstable-msc3927 = ["ruma-events?/unstable-msc3927"] | unstable-msc3927 = ["ruma-events?/unstable-msc3927"] | ||||||
| unstable-msc3930 = ["ruma-common/unstable-msc3930"] | unstable-msc3930 = ["ruma-common/unstable-msc3930"] | ||||||
| unstable-msc3931 = ["ruma-common/unstable-msc3931"] | unstable-msc3931 = ["ruma-common/unstable-msc3931"] | ||||||
| @ -268,6 +266,7 @@ unstable-msc4075 = ["ruma-events?/unstable-msc4075"] | |||||||
| unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"] | unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"] | ||||||
| unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"] | unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"] | ||||||
| unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"] | unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"] | ||||||
|  | unstable-msc4140 = ["ruma-client-api?/unstable-msc4140"] | ||||||
| unstable-pdu = ["ruma-events?/unstable-pdu"] | unstable-pdu = ["ruma-events?/unstable-pdu"] | ||||||
| unstable-unspecified = [ | unstable-unspecified = [ | ||||||
|     "ruma-common/unstable-unspecified", |     "ruma-common/unstable-unspecified", | ||||||
| @ -285,7 +284,6 @@ __ci = [ | |||||||
|     "unstable-msc2448", |     "unstable-msc2448", | ||||||
|     "unstable-msc2654", |     "unstable-msc2654", | ||||||
|     "unstable-msc2666", |     "unstable-msc2666", | ||||||
|     "unstable-msc2705", |  | ||||||
|     "unstable-msc2747", |     "unstable-msc2747", | ||||||
|     "unstable-msc2867", |     "unstable-msc2867", | ||||||
|     "unstable-msc2870", |     "unstable-msc2870", | ||||||
| @ -297,10 +295,10 @@ __ci = [ | |||||||
|     "unstable-msc3245-v1-compat", |     "unstable-msc3245-v1-compat", | ||||||
|     "unstable-msc3246", |     "unstable-msc3246", | ||||||
|     "unstable-msc3266", |     "unstable-msc3266", | ||||||
|     "unstable-msc3291", |  | ||||||
|     "unstable-msc3381", |     "unstable-msc3381", | ||||||
|     "unstable-msc3401", |     "unstable-msc3401", | ||||||
|     "unstable-msc3488", |     "unstable-msc3488", | ||||||
|  |     "unstable-msc3489", | ||||||
|     "unstable-msc3551", |     "unstable-msc3551", | ||||||
|     "unstable-msc3552", |     "unstable-msc3552", | ||||||
|     "unstable-msc3553", |     "unstable-msc3553", | ||||||
| @ -310,7 +308,6 @@ __ci = [ | |||||||
|     "unstable-msc3723", |     "unstable-msc3723", | ||||||
|     "unstable-msc3814", |     "unstable-msc3814", | ||||||
|     "unstable-msc3843", |     "unstable-msc3843", | ||||||
|     "unstable-msc3916", |  | ||||||
|     "unstable-msc3927", |     "unstable-msc3927", | ||||||
|     "unstable-msc3930", |     "unstable-msc3930", | ||||||
|     "unstable-msc3931", |     "unstable-msc3931", | ||||||
| @ -323,6 +320,7 @@ __ci = [ | |||||||
|     "unstable-msc4108", |     "unstable-msc4108", | ||||||
|     "unstable-msc4121", |     "unstable-msc4121", | ||||||
|     "unstable-msc4125", |     "unstable-msc4125", | ||||||
|  |     "unstable-msc4140" | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ const OLD_URL_WHITELIST: &[&str] = | |||||||
| 
 | 
 | ||||||
| /// Authorized versions in URLs pointing to the new specs.
 | /// Authorized versions in URLs pointing to the new specs.
 | ||||||
| const NEW_VERSION_WHITELIST: &[&str] = &[ | const NEW_VERSION_WHITELIST: &[&str] = &[ | ||||||
|     "v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7", "v1.8", "v1.9", "v1.10", |     "v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7", "v1.8", "v1.9", "v1.10", "v1.11", | ||||||
|     "latest", |     "latest", | ||||||
|     // This should only be enabled if a legitimate use case is found.
 |     // This should only be enabled if a legitimate use case is found.
 | ||||||
|     // "unstable",
 |     // "unstable",
 | ||||||
| @ -220,48 +220,72 @@ fn get_page_ids(url: &str) -> Result<HashMap<String, HasDuplicates>> { | |||||||
|             continue; |             continue; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  |         // For the URLs using the "latest" version, log the actual version we got.
 | ||||||
|  |         if url[URL_PREFIX.len()..].starts_with("latest/") { | ||||||
|  |             // Let's use the `meta` element with the `og:url` property, it contains the original
 | ||||||
|  |             // relative URL of the page.
 | ||||||
|  |             if tag.name.0 == b"meta" | ||||||
|  |                 && tag | ||||||
|  |                     .attributes | ||||||
|  |                     .get(b"property".as_slice()) | ||||||
|  |                     .is_some_and(|value| value.0 == b"og:url") | ||||||
|  |             { | ||||||
|  |                 match tag.attributes.get(b"content".as_slice()) { | ||||||
|  |                     Some(value) => { | ||||||
|  |                         println!( | ||||||
|  |                             "Original URL for latest spec page: {}", | ||||||
|  |                             String::from_utf8_lossy(value) | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                     None => println!( | ||||||
|  |                         "Could not get original URL for latest spec page: /{}", | ||||||
|  |                         &url[URL_PREFIX.len()..] | ||||||
|  |                     ), | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         let Some(id) = |         let Some(id) = | ||||||
|             tag.attributes.get(b"id".as_slice()).and_then(|s| String::from_utf8(s.0.clone()).ok()) |             tag.attributes.get(b"id".as_slice()).and_then(|s| String::from_utf8(s.0.clone()).ok()) | ||||||
|         else { |         else { | ||||||
|             continue; |             continue; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let (id, has_duplicates) = uniquify_heading_id(id, &mut ids); |         let has_duplicates = heading_id_has_duplicates(&id, &mut ids); | ||||||
|  | 
 | ||||||
|         ids.insert(id, has_duplicates); |         ids.insert(id, has_duplicates); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Ok(ids) |     Ok(ids) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Make sure the ID is unique in the page, if not make it unique.
 | /// Check whether the given heading ID has duplicates in the given map.
 | ||||||
| ///
 | ///
 | ||||||
| /// This is necessary because Matrix spec pages do that in JavaScript, so IDs
 | /// This check is necessary because duplicates IDs have a number depending on their occurrence in a
 | ||||||
| /// are not unique in the source.
 | /// HTML page. If a duplicate ID is added, moved or removed from the spec, its number might change
 | ||||||
| ///
 | /// from one version to the next.
 | ||||||
| /// This is a reimplementation of the algorithm used for the spec.
 | fn heading_id_has_duplicates( | ||||||
| ///
 |     id: &str, | ||||||
| /// See <https://github.com/matrix-org/matrix-spec/blob/6b02e393082570db2d0a651ddb79a365bc4a0f8d/static/js/toc.js#L25-L37>.
 |  | ||||||
| fn uniquify_heading_id( |  | ||||||
|     mut id: String, |  | ||||||
|     unique_ids: &mut HashMap<String, HasDuplicates>, |     unique_ids: &mut HashMap<String, HasDuplicates>, | ||||||
| ) -> (String, HasDuplicates) { | ) -> HasDuplicates { | ||||||
|     let base_id = id.clone(); |     // IDs that should be duplicates end with `-{number}`.
 | ||||||
|     let mut counter: u16 = 0; |     let Some((start, _end)) = | ||||||
|     let mut has_duplicates = HasDuplicates::No; |         id.rsplit_once('-').filter(|(_start, end)| end.chars().all(|c| c.is_ascii_digit())) | ||||||
|  |     else { | ||||||
|  |         return HasDuplicates::No; | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     while let Some(other_id_has_dup) = unique_ids.get_mut(&id) { |     // Update the first duplicate ID, because it doesn't end with a number.
 | ||||||
|         has_duplicates = HasDuplicates::Yes; |     if let Some(other_id_has_dup) = unique_ids.get_mut(start) { | ||||||
|         *other_id_has_dup = HasDuplicates::Yes; |         *other_id_has_dup = HasDuplicates::Yes; | ||||||
|         counter += 1; |  | ||||||
|         id = format!("{base_id}-{counter}"); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     (id, has_duplicates) |     HasDuplicates::Yes | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn print_link_err(error: &str, link: &SpecLink) { | fn print_link_err(error: &str, link: &SpecLink) { | ||||||
|     println!( |     println!( | ||||||
|         "\n{error}\nfile: {}:{}\nlink: {}", |         "\n{error}\n  file: {}:{}\n  link: {}", | ||||||
|         link.path.display(), |         link.path.display(), | ||||||
|         link.line, |         link.line, | ||||||
|         link.url.get(..80).unwrap_or(&link.url), |         link.url.get(..80).unwrap_or(&link.url), | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user