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
|
||||
|
||||
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:
|
||||
"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".
|
||||
|
@ -33,7 +33,7 @@ pub mod v1 {
|
||||
|
||||
/// 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).
|
||||
#[ruma_api(query_map)]
|
||||
#[ruma_api(query_all)]
|
||||
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.
|
||||
// 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>,
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ Breaking changes:
|
||||
as before.
|
||||
- Change type of `client_secret` field in `ThirdpartyIdCredentials`
|
||||
from `Box<ClientSecret>` to `OwnedClientSecret`
|
||||
- Make `id_server` and `id_access_token` in `ThirdpartyIdCredentials` optional
|
||||
|
||||
Improvements:
|
||||
|
||||
@ -13,15 +14,23 @@ Improvements:
|
||||
- Heroes in `sync::sync_events::v4`: `SyncRequestList` and `RoomSubscription`
|
||||
both have a new `include_heroes` field. `SlidingSyncRoom` has a new `heroes`
|
||||
field, with a new type `SlidingSyncRoomHero`.
|
||||
- Add unstable support for authenticated media endpoints, according to MSC3916.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Rename `avatar` to `avatar_url` when (De)serializing
|
||||
- Add support for authenticated media endpoints, according to MSC3916 / Matrix
|
||||
1.11.
|
||||
- They replace the newly deprecated `media::get_*` endpoints.
|
||||
- Stabilize support for animated thumbnails, according to Matrix 1.11
|
||||
- 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:
|
||||
|
||||
- Rename `avatar` to `avatar_url` when (De)serializing `SlidingSyncRoomHero`
|
||||
- `user_id` of `SlidingSyncRoomHero` is now mandatory
|
||||
- Make authentication with access token optional for the `change_password` and
|
||||
`deactivate` endpoints.
|
||||
|
||||
# 0.18.0
|
||||
|
||||
|
@ -40,7 +40,6 @@ unstable-exhaustive-types = ["ruma-common/unstable-exhaustive-types"]
|
||||
unstable-msc2666 = []
|
||||
unstable-msc2448 = []
|
||||
unstable-msc2654 = []
|
||||
unstable-msc2705 = []
|
||||
unstable-msc2965 = []
|
||||
unstable-msc2967 = []
|
||||
unstable-msc3266 = []
|
||||
@ -48,10 +47,10 @@ unstable-msc3488 = []
|
||||
unstable-msc3575 = []
|
||||
unstable-msc3814 = []
|
||||
unstable-msc3843 = []
|
||||
unstable-msc3916 = []
|
||||
unstable-msc3983 = []
|
||||
unstable-msc4108 = []
|
||||
unstable-msc4121 = []
|
||||
unstable-msc4140 = []
|
||||
|
||||
[dependencies]
|
||||
as_variant = { workspace = true }
|
||||
|
@ -17,7 +17,7 @@ pub mod v3 {
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: POST,
|
||||
rate_limited: true,
|
||||
authentication: AccessToken,
|
||||
authentication: AccessTokenOptional,
|
||||
history: {
|
||||
1.0 => "/_matrix/client/r0/account/password",
|
||||
1.1 => "/_matrix/client/v3/account/password",
|
||||
|
@ -20,7 +20,7 @@ pub mod v3 {
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: POST,
|
||||
rate_limited: true,
|
||||
authentication: AccessToken,
|
||||
authentication: AccessTokenOptional,
|
||||
history: {
|
||||
1.0 => "/_matrix/client/r0/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_as_filename;
|
||||
|
@ -2,10 +2,10 @@
|
||||
//!
|
||||
//! Retrieve content from the media store.
|
||||
|
||||
pub mod unstable {
|
||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
||||
pub mod v1 {
|
||||
//! `/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;
|
||||
|
||||
@ -17,10 +17,11 @@ pub mod unstable {
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
rate_limited: true,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
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.
|
||||
|
||||
pub mod unstable {
|
||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
||||
pub mod v1 {
|
||||
//! `/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;
|
||||
|
||||
@ -17,10 +17,11 @@ pub mod unstable {
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
rate_limited: true,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
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.
|
||||
|
||||
pub mod unstable {
|
||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
||||
pub mod v1 {
|
||||
//! `/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;
|
||||
|
||||
@ -24,6 +24,7 @@ pub mod unstable {
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
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.
|
||||
///
|
||||
/// When `true`, the server should return an animated thumbnail if possible and supported.
|
||||
/// Otherwise it must not return an animated thumbnail.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[cfg(feature = "unstable-msc2705")]
|
||||
/// When `Some(true)`, the server should return an animated thumbnail if possible and
|
||||
/// supported. When `Some(false)`, the server must not return an animated
|
||||
/// thumbnail. When `None`, the server should not return an animated thumbnail.
|
||||
#[ruma_api(query)]
|
||||
#[serde(
|
||||
rename = "org.matrix.msc2705.animated",
|
||||
default,
|
||||
skip_serializing_if = "ruma_common::serde::is_default"
|
||||
)]
|
||||
pub animated: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub animated: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response type for the `get_content_thumbnail` endpoint.
|
||||
@ -111,8 +106,7 @@ pub mod unstable {
|
||||
width,
|
||||
height,
|
||||
timeout_ms: crate::media::default_download_timeout(),
|
||||
#[cfg(feature = "unstable-msc2705")]
|
||||
animated: false,
|
||||
animated: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,10 @@
|
||||
//!
|
||||
//! Gets the config for the media repository.
|
||||
|
||||
pub mod unstable {
|
||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
||||
pub mod v1 {
|
||||
//! `/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 ruma_common::{
|
||||
@ -19,6 +19,7 @@ pub mod unstable {
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
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.
|
||||
|
||||
pub mod unstable {
|
||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
||||
pub mod v1 {
|
||||
//! `/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::{
|
||||
api::{request, response, Metadata},
|
||||
@ -20,6 +20,7 @@ pub mod unstable {
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
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 alias;
|
||||
pub mod appservice;
|
||||
#[cfg(feature = "unstable-msc3916")]
|
||||
pub mod authenticated_media;
|
||||
pub mod backup;
|
||||
pub mod config;
|
||||
@ -24,6 +23,8 @@ pub mod directory;
|
||||
pub mod discovery;
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
#[cfg(feature = "unstable-msc4140")]
|
||||
pub mod future;
|
||||
pub mod http_headers;
|
||||
pub mod keys;
|
||||
pub mod knock;
|
||||
|
@ -24,11 +24,16 @@ pub mod v3 {
|
||||
history: {
|
||||
1.0 => "/_matrix/media/r0/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(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 {
|
||||
/// The server name from the mxc:// URI (the authoritory component).
|
||||
#[ruma_api(path)]
|
||||
@ -106,6 +111,7 @@ pub mod v3 {
|
||||
pub cache_control: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given media ID and server name.
|
||||
pub fn new(media_id: String, server_name: OwnedServerName) -> Self {
|
||||
|
@ -24,11 +24,16 @@ pub mod v3 {
|
||||
history: {
|
||||
1.0 => "/_matrix/media/r0/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(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 {
|
||||
/// The server name from the mxc:// URI (the authoritory component).
|
||||
#[ruma_api(path)]
|
||||
@ -110,6 +115,7 @@ pub mod v3 {
|
||||
pub cache_control: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Request {
|
||||
/// 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 {
|
||||
|
@ -27,11 +27,16 @@ pub mod v3 {
|
||||
history: {
|
||||
1.0 => "/_matrix/media/r0/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(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 {
|
||||
/// The server name from the mxc:// URI (the authoritory component).
|
||||
#[ruma_api(path)]
|
||||
@ -90,18 +95,12 @@ pub mod v3 {
|
||||
|
||||
/// Whether the server should return an animated thumbnail.
|
||||
///
|
||||
/// When `true`, the server should return an animated thumbnail if possible and supported.
|
||||
/// Otherwise it must not return an animated thumbnail.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[cfg(feature = "unstable-msc2705")]
|
||||
/// When `Some(true)`, the server should return an animated thumbnail if possible and
|
||||
/// supported. When `Some(false)`, the server must not return an animated
|
||||
/// thumbnail. When `None`, the server should not return an animated thumbnail.
|
||||
#[ruma_api(query)]
|
||||
#[serde(
|
||||
rename = "org.matrix.msc2705.animated",
|
||||
default,
|
||||
skip_serializing_if = "ruma_common::serde::is_default"
|
||||
)]
|
||||
pub animated: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub animated: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response type for the `get_content_thumbnail` endpoint.
|
||||
@ -141,6 +140,7 @@ pub mod v3 {
|
||||
pub content_disposition: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given media ID, server name, desired thumbnail width
|
||||
/// and desired thumbnail height.
|
||||
@ -159,8 +159,7 @@ pub mod v3 {
|
||||
allow_remote: true,
|
||||
timeout_ms: crate::media::default_download_timeout(),
|
||||
allow_redirect: false,
|
||||
#[cfg(feature = "unstable-msc2705")]
|
||||
animated: false,
|
||||
animated: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,12 +20,17 @@ pub mod v3 {
|
||||
history: {
|
||||
1.0 => "/_matrix/media/r0/config",
|
||||
1.1 => "/_matrix/media/v3/config",
|
||||
1.11 => deprecated,
|
||||
}
|
||||
};
|
||||
|
||||
/// Request type for the `get_media_config` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[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 {}
|
||||
|
||||
/// Response type for the `get_media_config` endpoint.
|
||||
@ -36,6 +41,7 @@ pub mod v3 {
|
||||
pub upload_size: UInt,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Request {
|
||||
/// Creates an empty `Request`.
|
||||
pub fn new() -> Self {
|
||||
|
@ -21,11 +21,16 @@ pub mod v3 {
|
||||
history: {
|
||||
1.0 => "/_matrix/media/r0/preview_url",
|
||||
1.1 => "/_matrix/media/v3/preview_url",
|
||||
1.11 => deprecated,
|
||||
}
|
||||
};
|
||||
|
||||
/// Request type for the `get_media_preview` endpoint.
|
||||
#[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 {
|
||||
/// URL to get a preview of.
|
||||
#[ruma_api(query)]
|
||||
@ -49,6 +54,7 @@ pub mod v3 {
|
||||
pub data: Option<Box<RawJsonValue>>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given url.
|
||||
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 third party identifier][spec-3pid].
|
||||
//!
|
||||
//! [spec-mxid]: https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3roomsroomidinvite
|
||||
//! [spec-3pid]: https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3roomsroomidinvite-1
|
||||
//! [spec-mxid]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3roomsroomidinvite
|
||||
//! [spec-3pid]: https://spec.matrix.org/latest/client-server-api/#thirdparty_post_matrixclientv3roomsroomidinvite
|
||||
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
|
@ -34,7 +34,7 @@ pub mod v3 {
|
||||
|
||||
/// 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).
|
||||
#[ruma_api(query_map)]
|
||||
#[ruma_api(query_all)]
|
||||
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.
|
||||
// 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>,
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,11 @@ pub enum AuthData {
|
||||
/// Fallback acknowledgement.
|
||||
FallbackAcknowledgement(FallbackAcknowledgement),
|
||||
|
||||
/// Terms of service (`m.login.terms`).
|
||||
///
|
||||
/// This type is only valid during account registration.
|
||||
Terms(Terms),
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(CustomAuthData),
|
||||
}
|
||||
@ -90,6 +95,7 @@ impl AuthData {
|
||||
"m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?),
|
||||
"m.login.dummy" => Self::Dummy(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 })
|
||||
}
|
||||
@ -111,6 +117,7 @@ impl AuthData {
|
||||
Self::Dummy(_) => Some(AuthType::Dummy),
|
||||
Self::RegistrationToken(_) => Some(AuthType::RegistrationToken),
|
||||
Self::FallbackAcknowledgement(_) => None,
|
||||
Self::Terms(_) => Some(AuthType::Terms),
|
||||
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::RegistrationToken(x) => x.session.as_deref(),
|
||||
Self::FallbackAcknowledgement(x) => Some(&x.session),
|
||||
Self::Terms(x) => x.session.as_deref(),
|
||||
Self::_Custom(x) => x.session.as_deref(),
|
||||
}
|
||||
}
|
||||
@ -165,8 +173,10 @@ impl AuthData {
|
||||
Self::RegistrationToken(x) => {
|
||||
Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None }))
|
||||
}
|
||||
// Dummy and fallback acknowledgement have no associated data
|
||||
Self::Dummy(_) | Self::FallbackAcknowledgement(_) => Cow::Owned(JsonObject::default()),
|
||||
// Dummy, fallback acknowledgement, and terms of service have no associated data
|
||||
Self::Dummy(_) | Self::FallbackAcknowledgement(_) | Self::Terms(_) => {
|
||||
Cow::Owned(JsonObject::default())
|
||||
}
|
||||
Self::_Custom(c) => Cow::Borrowed(&c.extra),
|
||||
}
|
||||
}
|
||||
@ -183,6 +193,7 @@ impl fmt::Debug for AuthData {
|
||||
Self::Dummy(inner) => inner.fmt(f),
|
||||
Self::RegistrationToken(inner) => inner.fmt(f),
|
||||
Self::FallbackAcknowledgement(inner) => inner.fmt(f),
|
||||
Self::Terms(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") => {
|
||||
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),
|
||||
Some(_) => from_raw_json_value(&json).map(Self::_Custom),
|
||||
}
|
||||
@ -253,6 +265,12 @@ pub enum AuthType {
|
||||
#[ruma_enum(rename = "m.login.registration_token")]
|
||||
RegistrationToken,
|
||||
|
||||
/// Terms of service (`m.login.terms`).
|
||||
///
|
||||
/// This type is only valid during account registration.
|
||||
#[ruma_enum(rename = "m.login.terms")]
|
||||
Terms,
|
||||
|
||||
#[doc(hidden)]
|
||||
_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)]
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[non_exhaustive]
|
||||
@ -551,29 +591,25 @@ pub struct IncomingCustomThirdPartyId {
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct ThirdpartyIdCredentials {
|
||||
/// Identity server session ID.
|
||||
/// Identity server (or homeserver) session ID.
|
||||
pub sid: OwnedSessionId,
|
||||
|
||||
/// Identity server client secret.
|
||||
/// Identity server (or homeserver) client secret.
|
||||
pub client_secret: OwnedClientSecret,
|
||||
|
||||
/// Identity server URL.
|
||||
pub id_server: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id_server: Option<String>,
|
||||
|
||||
/// Identity server access token.
|
||||
pub id_access_token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id_access_token: Option<String>,
|
||||
}
|
||||
|
||||
impl ThirdpartyIdCredentials {
|
||||
/// Creates a new `ThirdpartyIdCredentials` with the given session ID, client secret, identity
|
||||
/// server address and access token.
|
||||
pub fn new(
|
||||
sid: OwnedSessionId,
|
||||
client_secret: OwnedClientSecret,
|
||||
id_server: String,
|
||||
id_access_token: String,
|
||||
) -> Self {
|
||||
Self { sid, client_secret, id_server, id_access_token }
|
||||
/// Creates a new `ThirdpartyIdCredentials` with the given session ID and client secret.
|
||||
pub fn new(sid: OwnedSessionId, client_secret: OwnedClientSecret) -> Self {
|
||||
Self { sid, client_secret, id_server: None, id_access_token: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,30 @@
|
||||
# [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:
|
||||
|
||||
- Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for
|
||||
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
|
||||
|
||||
|
@ -122,9 +122,11 @@ macro_rules! metadata {
|
||||
/// 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
|
||||
/// string.
|
||||
/// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any
|
||||
/// type that implements `IntoIterator<Item = (String, String)>` (e.g. `HashMap<String,
|
||||
/// String>`, can be used for cases where an endpoint supports arbitrary query parameters.
|
||||
/// * `#[ruma_api(query_all)]`: Instead of individual query fields, one query_all field, of any
|
||||
/// type that can be (de)serialized by [serde_html_form], can be used for cases where
|
||||
/// 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]`
|
||||
/// attributes to customize (de)serialization.
|
||||
/// * `#[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 {}
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [serde_html_form]: https://crates.io/crates/serde_html_form
|
||||
pub use ruma_macros::request;
|
||||
/// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
|
||||
///
|
||||
/// The `OutgoingRequest` impl is feature-gated behind `cfg(feature = "client")`.
|
||||
/// The `IncomingRequest` impl is feature-gated behind `cfg(feature = "server")`.
|
||||
/// The `OutgoingResponse` 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.
|
||||
///
|
||||
@ -223,7 +227,7 @@ pub use ruma_macros::request;
|
||||
///
|
||||
/// ## 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
|
||||
/// 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
|
||||
/// 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,
|
||||
|
||||
/// Authentication is optional, and it is performed by including an access token in the
|
||||
/// `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,
|
||||
|
||||
/// Authentication is only performed for appservices, by including an access token in the
|
||||
/// `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,
|
||||
|
||||
/// 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/>.
|
||||
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 {
|
||||
@ -564,6 +569,7 @@ impl TryFrom<&str> for MatrixVersion {
|
||||
"v1.8" => V1_8,
|
||||
"v1.9" => V1_9,
|
||||
"v1.10" => V1_10,
|
||||
"v1.11" => V1_11,
|
||||
_ => return Err(UnknownVersionError),
|
||||
})
|
||||
}
|
||||
@ -613,6 +619,7 @@ impl MatrixVersion {
|
||||
MatrixVersion::V1_8 => (1, 8),
|
||||
MatrixVersion::V1_9 => (1, 9),
|
||||
MatrixVersion::V1_10 => (1, 10),
|
||||
MatrixVersion::V1_11 => (1, 11),
|
||||
}
|
||||
}
|
||||
|
||||
@ -630,6 +637,7 @@ impl MatrixVersion {
|
||||
(1, 8) => Ok(MatrixVersion::V1_8),
|
||||
(1, 9) => Ok(MatrixVersion::V1_9),
|
||||
(1, 10) => Ok(MatrixVersion::V1_10),
|
||||
(1, 11) => Ok(MatrixVersion::V1_11),
|
||||
_ => Err(UnknownVersionError),
|
||||
}
|
||||
}
|
||||
@ -724,7 +732,9 @@ impl MatrixVersion {
|
||||
// <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
|
||||
| MatrixVersion::V1_9
|
||||
// <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),
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
||||
@ -572,12 +575,11 @@ mod tests {
|
||||
.to_string(),
|
||||
"https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs"
|
||||
);
|
||||
assert_eq!(
|
||||
room_alias_id!("#ruma:notareal.hs")
|
||||
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
||||
.to_string(),
|
||||
"https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs"
|
||||
);
|
||||
#[allow(deprecated)]
|
||||
let uri = room_alias_id!("#ruma:notareal.hs")
|
||||
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
||||
.to_string();
|
||||
assert_eq!(uri, "https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs");
|
||||
assert_eq!(
|
||||
room_id!("!ruma:notareal.hs")
|
||||
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
||||
@ -869,12 +871,11 @@ mod tests {
|
||||
.to_string(),
|
||||
"matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
|
||||
);
|
||||
assert_eq!(
|
||||
room_alias_id!("#ruma:notareal.hs")
|
||||
.matrix_event_uri(event_id!("$event:notareal.hs"))
|
||||
.to_string(),
|
||||
"matrix:r/ruma:notareal.hs/e/event:notareal.hs"
|
||||
);
|
||||
#[allow(deprecated)]
|
||||
let uri = room_alias_id!("#ruma:notareal.hs")
|
||||
.matrix_event_uri(event_id!("$event:notareal.hs"))
|
||||
.to_string();
|
||||
assert_eq!(uri, "matrix:r/ruma:notareal.hs/e/event:notareal.hs");
|
||||
assert_eq!(
|
||||
room_id!("!ruma: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.
|
||||
///
|
||||
/// 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 {
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
MatrixUri::new((self.to_owned(), ev_id.into()).into(), Vec::new(), None)
|
||||
}
|
||||
|
@ -602,7 +602,7 @@ mod tests {
|
||||
assert!(!"m".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!("exple".matches_word("ex*ple"));
|
||||
assert!("An exciting triple-whammy".matches_word("ex*ple"));
|
||||
@ -651,7 +651,7 @@ mod tests {
|
||||
assert!("".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".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
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -91,7 +94,8 @@ pub struct ProtocolInstance {
|
||||
///
|
||||
/// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
|
||||
#[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`.
|
||||
@ -109,30 +113,18 @@ pub struct ProtocolInstanceInit {
|
||||
|
||||
/// A unique identifier across all instances.
|
||||
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 {
|
||||
fn from(init: ProtocolInstanceInit) -> Self {
|
||||
let ProtocolInstanceInit {
|
||||
desc,
|
||||
fields,
|
||||
network_id,
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
instance_id,
|
||||
} = init;
|
||||
let ProtocolInstanceInit { desc, fields, network_id } = init;
|
||||
Self {
|
||||
desc,
|
||||
icon: None,
|
||||
fields,
|
||||
network_id,
|
||||
#[cfg(feature = "unstable-unspecified")]
|
||||
instance_id,
|
||||
instance_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,7 +232,6 @@ pub enum Medium {
|
||||
/// this type using `ThirdPartyIdentifier::Init` / `.into()`.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ThirdPartyIdentifier {
|
||||
/// The third party identifier address.
|
||||
pub address: String,
|
||||
@ -255,6 +246,20 @@ pub struct ThirdPartyIdentifier {
|
||||
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`.
|
||||
///
|
||||
/// 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::{
|
||||
api::{request, response, 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]
|
||||
pub struct Request {
|
||||
#[ruma_api(query_map)]
|
||||
#[ruma_api(query_all)]
|
||||
pub fields: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// Response type for the `newtype_body_endpoint` endpoint.
|
||||
/// Response type for the `query_all_vec_endpoint` endpoint.
|
||||
#[response]
|
||||
pub struct Response {}
|
||||
}
|
||||
|
@ -1,12 +1,25 @@
|
||||
# [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:
|
||||
|
||||
- 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:
|
||||
|
||||
- `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
|
||||
|
||||
|
@ -16,7 +16,7 @@ all-features = true
|
||||
[features]
|
||||
canonical-json = ["ruma-common/canonical-json"]
|
||||
html = ["dep:ruma-html"]
|
||||
markdown = ["pulldown-cmark"]
|
||||
markdown = ["dep:pulldown-cmark"]
|
||||
unstable-exhaustive-types = []
|
||||
unstable-msc1767 = []
|
||||
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
|
||||
unstable-msc3245-v1-compat = []
|
||||
unstable-msc3246 = ["unstable-msc3927"]
|
||||
unstable-msc3291 = []
|
||||
unstable-msc3381 = ["unstable-msc1767"]
|
||||
unstable-msc3401 = []
|
||||
unstable-msc3488 = ["unstable-msc1767"]
|
||||
unstable-msc3489 = ["unstable-msc3488"]
|
||||
unstable-msc3551 = ["unstable-msc3956"]
|
||||
unstable-msc3552 = ["unstable-msc3551"]
|
||||
unstable-msc3553 = ["unstable-msc3552"]
|
||||
@ -41,7 +41,7 @@ unstable-msc3927 = ["unstable-msc3551"]
|
||||
unstable-msc3954 = ["unstable-msc1767"]
|
||||
unstable-msc3955 = ["unstable-msc1767"]
|
||||
unstable-msc3956 = ["unstable-msc1767"]
|
||||
unstable-msc4075 = []
|
||||
unstable-msc4075 = ["unstable-msc3401"]
|
||||
unstable-pdu = []
|
||||
|
||||
# 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_option = "0.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"] }
|
||||
ruma-common = { workspace = 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")]
|
||||
pub mod notify;
|
||||
pub mod reject;
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
pub mod sdp_stream_metadata_changed;
|
||||
pub mod select_answer;
|
||||
|
||||
@ -59,14 +58,12 @@ pub struct StreamMetadata {
|
||||
/// Whether the audio track of the stream is muted.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub audio_muted: bool,
|
||||
|
||||
/// Whether the video track of the stream is muted.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub video_muted: bool,
|
||||
}
|
||||
@ -74,13 +71,7 @@ pub struct StreamMetadata {
|
||||
impl StreamMetadata {
|
||||
/// Creates a new `StreamMetadata` with the given purpose.
|
||||
pub fn new(purpose: StreamPurpose) -> Self {
|
||||
Self {
|
||||
purpose,
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
audio_muted: false,
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
video_muted: false,
|
||||
}
|
||||
Self { purpose, audio_muted: false, 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.
|
||||
//!
|
||||
//! [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;
|
||||
use ruma_common::{serde::StringEnum, MilliSecondsSinceUnixEpoch, OwnedUserId};
|
||||
use ruma_macros::EventContent;
|
||||
pub use focus::*;
|
||||
pub use member_data::*;
|
||||
use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedUserId};
|
||||
use ruma_macros::{EventContent, StringEnum};
|
||||
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
|
||||
/// matrixRTC session. It consists of memberships / sessions.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
/// This is the object containing all the data related to a Matrix users participation in a
|
||||
/// MatrixRTC session.
|
||||
///
|
||||
/// 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)]
|
||||
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = OwnedUserId)]
|
||||
pub struct CallMemberEventContent {
|
||||
/// A list of all the memberships that user currently has in this room.
|
||||
///
|
||||
/// There can be multiple ones in cases 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).
|
||||
pub memberships: Vec<Membership>,
|
||||
#[serde(untagged)]
|
||||
pub enum CallMemberEventContent {
|
||||
/// The legacy format for m.call.member events. (An array of memberships. The devices of one
|
||||
/// user.)
|
||||
LegacyContent(LegacyMembershipContent),
|
||||
/// Normal membership events. One event per membership. Multiple state keys will
|
||||
/// be used to describe multiple devices for one user.
|
||||
SessionContent(SessionMembershipData),
|
||||
/// An empty content means this user has been in a rtc session but is not anymore.
|
||||
Empty(EmptyMembershipData),
|
||||
}
|
||||
|
||||
impl CallMemberEventContent {
|
||||
/// Creates a new `CallMemberEventContent`.
|
||||
pub fn new(memberships: Vec<Membership>) -> Self {
|
||||
Self { memberships }
|
||||
/// Creates a new [`CallMemberEventContent`] with [`LegacyMembershipData`].
|
||||
pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// This copies all the memberships and converts them
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in case
|
||||
/// the Membership does not contain `created_ts`. (`origin_server_ts` will be ignored if
|
||||
/// `created_ts` is `Some`)
|
||||
/// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in the
|
||||
/// Membership does not contain [`LegacyMembershipData::created_ts`]. (`origin_server_ts` will
|
||||
/// be ignored if [`LegacyMembershipData::created_ts`] is `Some`)
|
||||
pub fn active_memberships(
|
||||
&self,
|
||||
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
) -> Vec<&Membership> {
|
||||
self.memberships.iter().filter(|m| !m.is_expired(origin_server_ts)).collect()
|
||||
) -> Vec<MembershipData<'_>> {
|
||||
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`.
|
||||
/// `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
|
||||
/// expired)
|
||||
pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
|
||||
self.memberships.iter_mut().for_each(|m| {
|
||||
m.created_ts.get_or_insert(origin_server_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)]
|
||||
#[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
|
||||
match self {
|
||||
CallMemberEventContent::LegacyContent(content) => {
|
||||
content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| {
|
||||
m.created_ts.get_or_insert(origin_server_ts);
|
||||
});
|
||||
}
|
||||
CallMemberEventContent::SessionContent(m) => {
|
||||
m.created_ts.get_or_insert(origin_server_ts);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial set of fields of [`Membership`].
|
||||
#[derive(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))]
|
||||
/// This describes the CallMember event if the user is not part of the current session.
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
||||
#[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),
|
||||
pub struct EmptyMembershipData {
|
||||
/// An empty call member state event can optionally contain a leave reason.
|
||||
/// If it is `None` the user has left the call ordinarily. (Intentional hangup)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub leave_reason: Option<LeaveReason>,
|
||||
}
|
||||
|
||||
/// The fields to describe livekit as an `active_foci`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[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"))]
|
||||
/// This is the optional value for an empty membership event content:
|
||||
/// [`CallMemberEventContent::Empty`]. It is used when the user disconnected and a Future ([MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140))
|
||||
/// was used to update the membership after the client was not reachable anymore.
|
||||
#[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,
|
||||
|
||||
pub enum LeaveReason {
|
||||
/// The user left the call by losing network connection or closing
|
||||
/// the client before it was able to send the leave event.
|
||||
LostConnection,
|
||||
#[doc(hidden)]
|
||||
_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)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use ruma_common::MilliSecondsSinceUnixEpoch as TS;
|
||||
use serde_json::json;
|
||||
use assert_matches2::assert_matches;
|
||||
use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId};
|
||||
use serde_json::{from_value as from_json_value, json};
|
||||
|
||||
use super::{
|
||||
Application, CallApplicationContent, CallMemberEventContent, CallScope, Focus,
|
||||
LivekitFocus, Membership,
|
||||
focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
|
||||
member_data::{
|
||||
Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
|
||||
},
|
||||
CallMemberEventContent,
|
||||
};
|
||||
use crate::{
|
||||
call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
|
||||
AnyStateEvent, StateEvent,
|
||||
};
|
||||
|
||||
fn create_call_member_event_content() -> CallMemberEventContent {
|
||||
CallMemberEventContent::new(vec![Membership {
|
||||
fn create_call_member_legacy_event_content() -> CallMemberEventContent {
|
||||
CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
|
||||
application: Application::Call(CallApplicationContent {
|
||||
call_id: "123456".to_owned(),
|
||||
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]
|
||||
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!({
|
||||
"memberships": [
|
||||
{
|
||||
@ -358,14 +336,62 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
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]
|
||||
fn deserialize_call_member_event_content() {
|
||||
let call_member_ev: CallMemberEventContent = CallMemberEventContent::new(vec![
|
||||
Membership {
|
||||
fn deserialize_legacy_call_member_event_content() {
|
||||
let call_member_ev = CallMemberEventContent::new_legacy(vec![
|
||||
LegacyMembershipData {
|
||||
application: Application::Call(CallApplicationContent {
|
||||
call_id: "123456".to_owned(),
|
||||
scope: CallScope::Room,
|
||||
@ -379,7 +405,7 @@ mod tests {
|
||||
membership_id: "0".to_owned(),
|
||||
created_ts: None,
|
||||
},
|
||||
Membership {
|
||||
LegacyMembershipData {
|
||||
application: Application::Call(CallApplicationContent {
|
||||
call_id: "".to_owned(),
|
||||
scope: CallScope::Room,
|
||||
@ -432,7 +458,85 @@ mod tests {
|
||||
|
||||
let ev_content: CallMemberEventContent =
|
||||
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) {
|
||||
@ -449,44 +553,59 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn membership_do_expire() {
|
||||
let content = create_call_member_event_content();
|
||||
fn memberships_do_expire() {
|
||||
let content_legacy = create_call_member_legacy_event_content();
|
||||
let (now, one_second_ago, two_hours_ago) = timestamps();
|
||||
|
||||
assert_eq!(
|
||||
content.active_memberships(Some(one_second_ago)),
|
||||
content.memberships.iter().collect::<Vec<&Membership>>()
|
||||
content_legacy.active_memberships(Some(one_second_ago)),
|
||||
content_legacy.memberships()
|
||||
);
|
||||
assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
|
||||
assert_eq!(
|
||||
content.active_memberships(Some(now)),
|
||||
content.memberships.iter().collect::<Vec<&Membership>>()
|
||||
content_legacy.active_memberships(Some(two_hours_ago)),
|
||||
(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]
|
||||
fn set_created_ts() {
|
||||
let mut content_now = create_call_member_event_content();
|
||||
let mut content_two_hours_ago = create_call_member_event_content();
|
||||
let mut content_one_second_ago = create_call_member_event_content();
|
||||
let mut content_now = create_call_member_legacy_event_content();
|
||||
let mut content_two_hours_ago = create_call_member_legacy_event_content();
|
||||
let mut content_one_second_ago = create_call_member_legacy_event_content();
|
||||
let (now, one_second_ago, two_hours_ago) = timestamps();
|
||||
|
||||
content_now.set_created_ts_if_none(now);
|
||||
content_one_second_ago.set_created_ts_if_none(one_second_ago);
|
||||
content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
|
||||
assert_eq!(
|
||||
content_now.active_memberships(None),
|
||||
content_now.memberships.iter().collect::<Vec<&Membership>>()
|
||||
);
|
||||
assert_eq!(content_now.active_memberships(None), content_now.memberships());
|
||||
|
||||
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!(
|
||||
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.
|
||||
content_two_hours_ago.set_created_ts_if_none(one_second_ago);
|
||||
// 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
|
||||
|
||||
@ -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.
|
||||
//!
|
||||
//! [`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;
|
||||
|
||||
@ -15,7 +15,7 @@ use super::StreamMetadata;
|
||||
/// This event is sent by any party when a stream metadata changes but no negotiation is required.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||
#[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 {
|
||||
/// A unique identifier for the call.
|
||||
pub call_id: OwnedVoipId,
|
||||
|
@ -95,7 +95,6 @@ pub trait ToDeviceEventContent: EventContent<EventType = ToDeviceEventType> {}
|
||||
/// Event content that can be deserialized with its event type.
|
||||
pub trait EventContentFromType: EventContent {
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
|
@ -45,9 +45,8 @@ event_enum! {
|
||||
"m.call.candidates" => super::call::candidates,
|
||||
"m.call.negotiate" => super::call::negotiate,
|
||||
"m.call.reject" => super::call::reject,
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
#[ruma_enum(alias = "m.call.sdp_stream_metadata_changed")]
|
||||
"org.matrix.call.sdp_stream_metadata_changed" => super::call::sdp_stream_metadata_changed,
|
||||
#[ruma_enum(alias = "org.matrix.call.sdp_stream_metadata_changed")]
|
||||
"m.call.sdp_stream_metadata_changed" => super::call::sdp_stream_metadata_changed,
|
||||
"m.call.select_answer" => super::call::select_answer,
|
||||
#[cfg(feature = "unstable-msc3954")]
|
||||
#[ruma_enum(alias = "m.emote")]
|
||||
@ -88,6 +87,9 @@ event_enum! {
|
||||
#[cfg(feature = "unstable-msc3381")]
|
||||
#[ruma_enum(ident = UnstablePollEnd)]
|
||||
"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.room.encrypted" => super::room::encrypted,
|
||||
"m.room.message" => super::room::message,
|
||||
@ -127,6 +129,9 @@ event_enum! {
|
||||
"m.room.topic" => super::room::topic,
|
||||
"m.space.child" => super::space::child,
|
||||
"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")]
|
||||
#[ruma_enum(alias = "m.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
|
||||
/// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise.
|
||||
pub fn relation(&self) -> Option<encrypted::Relation> {
|
||||
#[cfg(feature = "unstable-msc3489")]
|
||||
use super::beacon::BeaconEventContent;
|
||||
use super::key::verification::{
|
||||
accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent,
|
||||
done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent,
|
||||
@ -361,13 +368,16 @@ impl AnyMessageLikeEventContent {
|
||||
| Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => {
|
||||
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")]
|
||||
Self::PollStart(_) | Self::UnstablePollStart(_) => None,
|
||||
#[cfg(feature = "unstable-msc4075")]
|
||||
Self::CallNotify(_) => None,
|
||||
#[cfg(feature = "unstable-msc3291")]
|
||||
Self::CallSdpStreamMetadataChanged(_) => None,
|
||||
Self::CallNegotiate(_)
|
||||
Self::CallSdpStreamMetadataChanged(_)
|
||||
| Self::CallNegotiate(_)
|
||||
| Self::CallReject(_)
|
||||
| Self::CallSelectAnswer(_)
|
||||
| Self::CallAnswer(_)
|
||||
|
@ -165,7 +165,10 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches2::assert_matches;
|
||||
use ruma_common::{event_id, serde::Base64};
|
||||
use ruma_common::{
|
||||
event_id,
|
||||
serde::{Base64, Raw},
|
||||
};
|
||||
use serde_json::{
|
||||
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.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")]
|
||||
pub mod audio;
|
||||
#[cfg(feature = "unstable-msc3489")]
|
||||
pub mod beacon;
|
||||
#[cfg(feature = "unstable-msc3489")]
|
||||
pub mod beacon_info;
|
||||
pub mod call;
|
||||
pub mod direct;
|
||||
pub mod dummy;
|
||||
|
@ -37,7 +37,7 @@ impl From<Annotation> for ReactionEventContent {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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 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
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[serde(tag = "rel_type", rename = "m.replace")]
|
||||
pub struct Replacement {
|
||||
/// The ID of the event being replaced.
|
||||
pub event_id: OwnedEventId,
|
||||
|
@ -74,27 +74,11 @@ impl Serialize for Relation {
|
||||
st.serialize_field("m.in_reply_to", in_reply_to)?;
|
||||
st.end()
|
||||
}
|
||||
Relation::Replacement(data) => {
|
||||
RelationSerHelper { rel_type: "m.replace", data }.serialize(serializer)
|
||||
}
|
||||
Relation::Reference(data) => {
|
||||
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::Replacement(data) => data.serialize(serializer),
|
||||
Relation::Reference(data) => data.serialize(serializer),
|
||||
Relation::Annotation(data) => data.serialize(serializer),
|
||||
Relation::Thread(data) => data.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)]
|
||||
pub struct Restricted {
|
||||
/// Allow rules which describe conditions that allow joining a room.
|
||||
#[serde(default)]
|
||||
pub allow: Vec<AllowRule>,
|
||||
}
|
||||
|
||||
@ -324,6 +325,16 @@ mod tests {
|
||||
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]
|
||||
fn join_rule_to_space_room_join_rule() {
|
||||
assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into());
|
||||
|
@ -157,7 +157,7 @@ pub(super) enum RelationSerHelper {
|
||||
Replacement(ReplacementJsonRepr),
|
||||
|
||||
/// An event that belongs to a thread, with stable names.
|
||||
#[serde(rename = "m.thread")]
|
||||
#[serde(untagged)]
|
||||
Thread(Thread),
|
||||
|
||||
/// 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
|
||||
/// 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)]
|
||||
#[derive(Clone, Debug, Serialize, EventContent)]
|
||||
#[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.
|
||||
///
|
||||
/// 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)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct SecretStorageV1AesHmacSha2Properties {
|
||||
@ -182,7 +182,7 @@ mod tests {
|
||||
PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
|
||||
SecretStorageV1AesHmacSha2Properties,
|
||||
};
|
||||
use crate::{EventContentFromType, GlobalAccountDataEvent};
|
||||
use crate::{AnyGlobalAccountDataEvent, EventContentFromType, GlobalAccountDataEvent};
|
||||
|
||||
#[test]
|
||||
fn key_description_serialization() {
|
||||
@ -326,7 +326,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_serialization() {
|
||||
fn event_content_serialization() {
|
||||
let mut content = SecretStorageKeyEventContent::new(
|
||||
"my_key_id".into(),
|
||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
|
||||
@ -346,6 +346,31 @@ mod tests {
|
||||
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]
|
||||
fn event_deserialization() {
|
||||
let json = json!({
|
||||
@ -358,8 +383,8 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let ev =
|
||||
from_json_value::<GlobalAccountDataEvent<SecretStorageKeyEventContent>>(json).unwrap();
|
||||
let any_ev = from_json_value::<AnyGlobalAccountDataEvent>(json).unwrap();
|
||||
assert_matches!(any_ev, AnyGlobalAccountDataEvent::SecretStorageKey(ev));
|
||||
assert_eq!(ev.content.key_id, "my_key_id");
|
||||
assert_eq!(ev.content.name.unwrap(), "my_key");
|
||||
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;
|
||||
#[cfg(feature = "unstable-msc2747")]
|
||||
use assign::assign;
|
||||
@ -8,11 +5,6 @@ use js_int::uint;
|
||||
use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
||||
#[cfg(feature = "unstable-msc2747")]
|
||||
use ruma_events::call::CallCapabilities;
|
||||
#[cfg(feature = "unstable-msc4075")]
|
||||
use ruma_events::{
|
||||
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
|
||||
Mentions,
|
||||
};
|
||||
use ruma_events::{
|
||||
call::{
|
||||
answer::CallAnswerEventContent,
|
||||
@ -616,83 +608,3 @@ fn select_v1_answer_event_deserialization() {
|
||||
assert_eq!(content.selected_party_id, "6336");
|
||||
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 ruma_common::{owned_device_id, owned_event_id};
|
||||
use ruma_common::{owned_device_id, owned_event_id, serde::Raw};
|
||||
use ruma_events::{
|
||||
relation::{CustomRelation, InReplyTo, Reference, Thread},
|
||||
relation::{Annotation, CustomRelation, InReplyTo, Reference, Thread},
|
||||
room::encrypted::{
|
||||
EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement,
|
||||
RoomEncryptedEventContent,
|
||||
@ -82,6 +82,17 @@ fn content_no_relation_deserialization() {
|
||||
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]
|
||||
fn content_reply_serialization() {
|
||||
let content = RoomEncryptedEventContent::new(
|
||||
@ -149,6 +160,22 @@ fn content_reply_deserialization() {
|
||||
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]
|
||||
fn content_replacement_serialization() {
|
||||
let content = RoomEncryptedEventContent::new(
|
||||
@ -214,6 +241,22 @@ fn content_replacement_deserialization() {
|
||||
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]
|
||||
fn content_reference_serialization() {
|
||||
let content = RoomEncryptedEventContent::new(
|
||||
@ -279,6 +322,22 @@ fn content_reference_deserialization() {
|
||||
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]
|
||||
fn content_thread_serialization() {
|
||||
let content = RoomEncryptedEventContent::new(
|
||||
@ -357,9 +416,23 @@ fn content_thread_deserialization() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_annotation_serialization() {
|
||||
use ruma_events::relation::Annotation;
|
||||
fn content_thread_serialization_roundtrip() {
|
||||
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(
|
||||
encrypted_scheme(),
|
||||
Some(Relation::Annotation(Annotation::new(
|
||||
@ -429,6 +502,23 @@ fn content_annotation_deserialization() {
|
||||
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]
|
||||
fn custom_relation_deserialization() {
|
||||
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 js_int::uint;
|
||||
use ruma_common::{serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
||||
use ruma_events::{AnyMessageLikeEvent, MessageLikeEvent};
|
||||
use serde_json::{from_value as from_json_value, json};
|
||||
use ruma_common::{
|
||||
serde::{CanBeEmpty, Raw},
|
||||
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]
|
||||
fn ui() {
|
||||
@ -46,3 +53,53 @@ fn deserialize_message_event() {
|
||||
assert_eq!(content.call_id, "foofoo");
|
||||
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 beacon;
|
||||
mod beacon_info;
|
||||
mod call;
|
||||
mod encrypted;
|
||||
mod enums;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use assert_matches2::assert_matches;
|
||||
use assign::assign;
|
||||
use ruma_common::owned_event_id;
|
||||
use ruma_common::{owned_event_id, serde::Raw};
|
||||
use ruma_events::{
|
||||
relation::{CustomRelation, InReplyTo, Replacement, Thread},
|
||||
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]
|
||||
fn replacement_serialize() {
|
||||
let content = assign!(
|
||||
@ -111,6 +127,28 @@ fn replacement_deserialize() {
|
||||
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]
|
||||
fn thread_plain_serialize() {
|
||||
let content = assign!(
|
||||
@ -250,6 +288,25 @@ fn thread_unstable_deserialize() {
|
||||
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]
|
||||
fn custom_deserialize() {
|
||||
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,
|
||||
},
|
||||
AnySyncTimelineEvent, Mentions, MessageLikeUnsigned,
|
||||
AnySyncTimelineEvent, EventContent, Mentions, MessageLikeUnsigned, RawExt,
|
||||
};
|
||||
use serde_json::{
|
||||
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);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn reply_add_mentions() {
|
||||
let user = owned_user_id!("@user:example.org");
|
||||
|
@ -33,7 +33,7 @@ pub mod v1 {
|
||||
pub query_type: String,
|
||||
|
||||
/// The query parameters.
|
||||
#[ruma_api(query_map)]
|
||||
#[ruma_api(query_all)]
|
||||
pub params: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,13 @@
|
||||
# [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
|
||||
|
||||
Breaking Changes:
|
||||
|
@ -152,7 +152,7 @@ pub enum MatrixElement {
|
||||
/// [`<div>`], a content division element.
|
||||
///
|
||||
/// [`<div>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
|
||||
Div,
|
||||
Div(DivData),
|
||||
|
||||
/// [`<table>`], a table element.
|
||||
///
|
||||
@ -268,7 +268,10 @@ impl MatrixElement {
|
||||
}
|
||||
b"hr" => (Self::Hr, 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"thead" => (Self::Thead, 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
|
||||
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 {
|
||||
/// Construct an empty `SpanData`.
|
||||
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`.
|
||||
@ -629,6 +643,9 @@ impl SpanData {
|
||||
b"data-mx-spoiler" => {
|
||||
data.spoiler = Some(attr.value.clone());
|
||||
}
|
||||
b"data-mx-maths" => {
|
||||
data.maths = Some(attr.value.clone());
|
||||
}
|
||||
_ => {
|
||||
remaining_attrs.insert(attr.clone());
|
||||
}
|
||||
@ -722,3 +739,53 @@ impl ImageData {
|
||||
(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,
|
||||
"ol" => &ALLOWED_ATTRIBUTES_OL_STRICT,
|
||||
"code" => &ALLOWED_ATTRIBUTES_CODE_STRICT,
|
||||
"div" => &ALLOWED_ATTRIBUTES_DIV_STRICT,
|
||||
};
|
||||
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_IMG_STRICT: Set<&str> =
|
||||
phf_set! { "width", "height", "alt", "title", "src" };
|
||||
static ALLOWED_ATTRIBUTES_OL_STRICT: Set<&str> = phf_set! { "start" };
|
||||
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,
|
||||
/// with their replacement.
|
||||
|
@ -26,7 +26,8 @@ fn elements() {
|
||||
// `<div>` element.
|
||||
let div_node = html_children.next().unwrap();
|
||||
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.
|
||||
assert_eq!(div_element.attrs.len(), 1);
|
||||
|
||||
|
@ -5,10 +5,8 @@
|
||||
Bug fixes:
|
||||
|
||||
- Allow underscores (`_`) when validating MXC URIs.
|
||||
- They have always been allowed in [the spec][mxc validation spec]
|
||||
in order to support URL-safe base64-encoded media IDs.
|
||||
|
||||
[mxc validation spec]: https://spec.matrix.org/v1.9/client-server-api/#security-considerations-5
|
||||
- They have always been allowed in the spec in order to support URL-safe
|
||||
base64-encoded media IDs.
|
||||
|
||||
Improvements:
|
||||
|
||||
|
@ -77,7 +77,7 @@ pub enum MxcUriError {
|
||||
/// Media identifier malformed due to invalid characters detected.
|
||||
///
|
||||
/// 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")]
|
||||
MediaIdMalformed,
|
||||
|
||||
|
@ -17,7 +17,7 @@ pub fn validate(uri: &str) -> Result<NonZeroU8, MxcUriError> {
|
||||
|
||||
let server_name = &uri[..index];
|
||||
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
|
||||
.bytes()
|
||||
.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!(path);
|
||||
syn::custom_keyword!(query);
|
||||
syn::custom_keyword!(query_map);
|
||||
syn::custom_keyword!(query_all);
|
||||
syn::custom_keyword!(header);
|
||||
syn::custom_keyword!(error);
|
||||
syn::custom_keyword!(manual_body_serde);
|
||||
@ -22,7 +22,7 @@ pub enum RequestMeta {
|
||||
RawBody,
|
||||
Path,
|
||||
Query,
|
||||
QueryMap,
|
||||
QueryAll,
|
||||
Header(Ident),
|
||||
}
|
||||
|
||||
@ -41,9 +41,9 @@ impl Parse for RequestMeta {
|
||||
} else if lookahead.peek(kw::query) {
|
||||
let _: kw::query = input.parse()?;
|
||||
Ok(Self::Query)
|
||||
} else if lookahead.peek(kw::query_map) {
|
||||
let _: kw::query_map = input.parse()?;
|
||||
Ok(Self::QueryMap)
|
||||
} else if lookahead.peek(kw::query_all) {
|
||||
let _: kw::query_all = input.parse()?;
|
||||
Ok(Self::QueryAll)
|
||||
} else if lookahead.peek(kw::header) {
|
||||
let _: kw::header = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
|
@ -137,8 +137,8 @@ impl Request {
|
||||
self.fields.iter().find_map(RequestField::as_raw_body_field)
|
||||
}
|
||||
|
||||
fn query_map_field(&self) -> Option<&Field> {
|
||||
self.fields.iter().find_map(RequestField::as_query_map_field)
|
||||
fn query_all_field(&self) -> Option<&Field> {
|
||||
self.fields.iter().find_map(RequestField::as_query_all_field)
|
||||
}
|
||||
|
||||
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 = PrivateField(&field);
|
||||
Some(quote! { (#field); })
|
||||
@ -220,15 +220,15 @@ impl Request {
|
||||
}
|
||||
};
|
||||
|
||||
let query_map_fields =
|
||||
self.fields.iter().filter(|f| matches!(&f.kind, RequestFieldKind::QueryMap));
|
||||
let has_query_map_field = match query_map_fields.count() {
|
||||
let query_all_fields =
|
||||
self.fields.iter().filter(|f| matches!(&f.kind, RequestFieldKind::QueryAll));
|
||||
let has_query_all_field = match query_all_fields.count() {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&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(
|
||||
&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.
|
||||
Query,
|
||||
|
||||
/// Data that appears in the query string as dynamic key-value pairs.
|
||||
QueryMap,
|
||||
/// Data that represents all the query string as a single type.
|
||||
QueryAll,
|
||||
}
|
||||
|
||||
impl RequestField {
|
||||
@ -319,7 +319,7 @@ impl RequestField {
|
||||
Some(RequestMeta::RawBody) => RequestFieldKind::RawBody,
|
||||
Some(RequestMeta::Path) => RequestFieldKind::Path,
|
||||
Some(RequestMeta::Query) => RequestFieldKind::Query,
|
||||
Some(RequestMeta::QueryMap) => RequestFieldKind::QueryMap,
|
||||
Some(RequestMeta::QueryAll) => RequestFieldKind::QueryAll,
|
||||
Some(RequestMeta::Header(header)) => RequestFieldKind::Header(header),
|
||||
None => RequestFieldKind::Body,
|
||||
};
|
||||
@ -359,10 +359,10 @@ impl RequestField {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the contained field if this request field is a query map kind.
|
||||
pub fn as_query_map_field(&self) -> Option<&Field> {
|
||||
/// Return the contained field if this request field is a query all kind.
|
||||
pub fn as_query_all_field(&self) -> Option<&Field> {
|
||||
match &self.kind {
|
||||
RequestFieldKind::QueryMap => Some(&self.inner),
|
||||
RequestFieldKind::QueryAll => Some(&self.inner),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ impl Request {
|
||||
(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 =
|
||||
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");
|
||||
|
@ -15,29 +15,11 @@ impl Request {
|
||||
let path_fields =
|
||||
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");
|
||||
|
||||
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);
|
||||
assert_trait_impl(&request_query.0);
|
||||
|
||||
&#serde_html_form::to_string(request_query)?
|
||||
}}
|
||||
|
@ -170,7 +170,17 @@ fn expand_deserialize_impl(
|
||||
};
|
||||
let self_variant = variant.ctor(quote! { Self });
|
||||
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! {
|
||||
#variant_attrs #(#ev_types)|* => {
|
||||
@ -328,6 +338,55 @@ fn expand_content_enum(
|
||||
let serialize_custom_event_error_path =
|
||||
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! {
|
||||
#( #attrs )*
|
||||
#[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]
|
||||
impl #ruma_events::#sub_trait_name for #ident {
|
||||
#state_event_content_impl
|
||||
|
@ -4,6 +4,19 @@ Breaking changes:
|
||||
|
||||
- The `XMatrix::new` method now takes `OwnedServerName` instead of `Option<OwnedServerName>`
|
||||
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
|
||||
|
||||
|
@ -16,9 +16,11 @@ all-features = true
|
||||
|
||||
[dependencies]
|
||||
headers = "0.4.0"
|
||||
http = { workspace = true }
|
||||
http-auth = { version = "0.1.9", default-features = false }
|
||||
ruma-common = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
yap = "0.12.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.3.16"
|
||||
|
@ -1,30 +1,38 @@
|
||||
//! Common types for implementing federation authorization.
|
||||
|
||||
use headers::{authorization::Credentials, HeaderValue};
|
||||
use ruma_common::{OwnedServerName, OwnedServerSigningKeyId};
|
||||
use std::{borrow::Cow, fmt, str::FromStr};
|
||||
|
||||
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 yap::{IntoTokens, TokenLocation, Tokens};
|
||||
|
||||
/// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
|
||||
/// [Matrix Server-Server API][spec]. Includes an implementation of
|
||||
/// [`headers::authorization::Credentials`] for automatically handling the encoding and decoding
|
||||
/// when using a web framework that supports typed headers.
|
||||
/// [Matrix Server-Server API][spec].
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
|
||||
#[derive(Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct XMatrix {
|
||||
/// The server name of the sending server.
|
||||
pub origin: OwnedServerName,
|
||||
/// The server name of the receiving sender. For compatibility with older servers, recipients
|
||||
/// 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
|
||||
/// must deny the request with an HTTP status code 401 Unauthorized.
|
||||
/// The server name of the receiving sender.
|
||||
///
|
||||
/// For compatibility with older servers, recipients 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 must deny the request with
|
||||
/// an HTTP status code 401 Unauthorized.
|
||||
pub destination: Option<OwnedServerName>,
|
||||
/// The ID - including the algorithm name - of the sending server's key that was used to sign
|
||||
/// the request.
|
||||
pub key: OwnedServerSigningKeyId,
|
||||
/// The signature of the JSON.
|
||||
pub sig: String,
|
||||
pub sig: Base64,
|
||||
}
|
||||
|
||||
impl XMatrix {
|
||||
@ -33,227 +41,214 @@ impl XMatrix {
|
||||
origin: OwnedServerName,
|
||||
destination: OwnedServerName,
|
||||
key: OwnedServerSigningKeyId,
|
||||
sig: String,
|
||||
sig: Base64,
|
||||
) -> Self {
|
||||
Self { origin, destination: Some(destination), key, sig }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_token<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
||||
tokens.optional(|t| {
|
||||
let token: Vec<u8> = t.take_while(|c| is_tchar(**c)).as_iter().copied().collect();
|
||||
if !token.is_empty() {
|
||||
Some(token)
|
||||
} else {
|
||||
debug!("Returning early because of empty token at {}", t.location().offset());
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Parse an X-Matrix Authorization header from the given string.
|
||||
pub fn parse(s: impl AsRef<str>) -> Result<Self, XMatrixParseError> {
|
||||
let parser = ChallengeParser::new(s.as_ref());
|
||||
let mut xmatrix = None;
|
||||
|
||||
fn parse_token_with_colons<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
||||
tokens.optional(|t| {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
for challenge in parser {
|
||||
let challenge = challenge?;
|
||||
|
||||
fn parse_quoted<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
||||
tokens.optional(|t| {
|
||||
if !(t.token(&b'"')) {
|
||||
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;
|
||||
}
|
||||
if challenge.scheme.eq_ignore_ascii_case(XMatrix::SCHEME) {
|
||||
xmatrix = Some(challenge);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(buffer)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_xmatrix_field<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<(String, Vec<u8>)> {
|
||||
tokens.optional(|t| {
|
||||
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
|
||||
}
|
||||
}
|
||||
})?;
|
||||
let Some(xmatrix) = xmatrix else {
|
||||
return Err(XMatrixParseError::NotFound);
|
||||
};
|
||||
|
||||
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 destination = None;
|
||||
let mut key = None;
|
||||
let mut sig = None;
|
||||
|
||||
for (name, value) in t.sep_by(|t| parse_xmatrix_field(t), |t| t.token(&b',')).as_iter() {
|
||||
match name.as_str() {
|
||||
"origin" => {
|
||||
if origin.is_some() {
|
||||
debug!("Field origin duplicated in X-Matrix Authorization header");
|
||||
}
|
||||
origin = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
||||
for (name, value) in xmatrix.params {
|
||||
if name.eq_ignore_ascii_case("origin") {
|
||||
if origin.is_some() {
|
||||
return Err(XMatrixParseError::DuplicateParameter("origin".to_owned()));
|
||||
} else {
|
||||
origin = Some(OwnedServerName::try_from(value.to_unescaped())?);
|
||||
}
|
||||
"destination" => {
|
||||
if destination.is_some() {
|
||||
debug!("Field destination duplicated in X-Matrix Authorization header");
|
||||
}
|
||||
destination = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
||||
} else if name.eq_ignore_ascii_case("destination") {
|
||||
if destination.is_some() {
|
||||
return Err(XMatrixParseError::DuplicateParameter("destination".to_owned()));
|
||||
} else {
|
||||
destination = Some(OwnedServerName::try_from(value.to_unescaped())?);
|
||||
}
|
||||
"key" => {
|
||||
if key.is_some() {
|
||||
debug!("Field key duplicated in X-Matrix Authorization header");
|
||||
}
|
||||
key = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
||||
} else if name.eq_ignore_ascii_case("key") {
|
||||
if key.is_some() {
|
||||
return Err(XMatrixParseError::DuplicateParameter("key".to_owned()));
|
||||
} else {
|
||||
key = Some(OwnedServerSigningKeyId::try_from(value.to_unescaped())?);
|
||||
}
|
||||
"sig" => {
|
||||
if sig.is_some() {
|
||||
debug!("Field sig duplicated in X-Matrix Authorization header");
|
||||
}
|
||||
sig = Some(std::str::from_utf8(&value).ok()?.to_owned());
|
||||
}
|
||||
name => {
|
||||
debug!("Unknown field {} found in X-Matrix Authorization header", name);
|
||||
} else if name.eq_ignore_ascii_case("sig") {
|
||||
if sig.is_some() {
|
||||
return Err(XMatrixParseError::DuplicateParameter("sig".to_owned()));
|
||||
} else {
|
||||
sig = Some(Base64::parse(value.to_unescaped())?);
|
||||
}
|
||||
} 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 {
|
||||
(0x41..=0x5A).contains(&c) || (0x61..=0x7A).contains(&c)
|
||||
impl fmt::Debug for XMatrix {
|
||||
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 {
|
||||
(0x30..=0x39).contains(&c)
|
||||
/// Whether the given char is a [token char].
|
||||
///
|
||||
/// [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 {
|
||||
const TOKEN_CHARS: [u8; 15] =
|
||||
[b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'.', b'^', b'_', b'`', b'|', b'~'];
|
||||
is_alpha(c) || is_digit(c) || TOKEN_CHARS.contains(&c)
|
||||
/// If the field value does not contain only token chars, convert it to a [quoted string].
|
||||
///
|
||||
/// [quoted string]: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4
|
||||
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 {
|
||||
c == b'\t'
|
||||
|| c == b' '
|
||||
|| c == 0x21
|
||||
|| (0x23..=0x5B).contains(&c)
|
||||
|| (0x5D..=0x7E).contains(&c)
|
||||
|| is_obs_text(c)
|
||||
impl fmt::Display for XMatrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Self { origin, destination, key, sig } = self;
|
||||
|
||||
let origin = escape_field_value(origin.as_str());
|
||||
let key = escape_field_value(key.as_str());
|
||||
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 {
|
||||
c >= 0x80 // The spec does contain an upper limit of 0xFF here, but that's enforced by the type
|
||||
impl FromStr for XMatrix {
|
||||
type Err = XMatrixParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_vchar(c: u8) -> bool {
|
||||
(0x21..=0x7E).contains(&c)
|
||||
impl TryFrom<&HeaderValue> for XMatrix {
|
||||
type Error = XMatrixParseError;
|
||||
|
||||
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
|
||||
Self::parse(value.to_str()?)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_quoted_pair(c: u8) -> bool {
|
||||
c == b'\t' || c == b' ' || is_vchar(c) || is_obs_text(c)
|
||||
impl From<&XMatrix> for HeaderValue {
|
||||
fn from(value: &XMatrix) -> Self {
|
||||
value.to_string().try_into().expect("header format is static")
|
||||
}
|
||||
}
|
||||
|
||||
impl Credentials for XMatrix {
|
||||
const SCHEME: &'static str = "X-Matrix";
|
||||
|
||||
fn decode(value: &HeaderValue) -> Option<Self> {
|
||||
let value: Vec<u8> = value.as_bytes().to_vec();
|
||||
parse_xmatrix(&mut value.into_tokens())
|
||||
value.try_into().ok()
|
||||
}
|
||||
|
||||
fn encode(&self) -> HeaderValue {
|
||||
if let Some(destination) = &self.destination {
|
||||
format!(
|
||||
"X-Matrix origin=\"{}\",destination=\"{destination}\",key=\"{}\",sig=\"{}\"",
|
||||
self.origin, self.key, self.sig
|
||||
)
|
||||
} else {
|
||||
format!("X-Matrix origin=\"{}\",key=\"{}\",sig=\"{}\"", self.origin, self.key, self.sig)
|
||||
}
|
||||
.try_into()
|
||||
.expect("header format is static")
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error when trying to parse an X-Matrix Authorization header.
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum XMatrixParseError {
|
||||
/// The `HeaderValue` could not be converted to a `str`.
|
||||
#[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)]
|
||||
mod tests {
|
||||
use headers::{authorization::Credentials, HeaderValue};
|
||||
use ruma_common::OwnedServerName;
|
||||
use ruma_common::{serde::Base64, OwnedServerName};
|
||||
|
||||
use super::XMatrix;
|
||||
|
||||
#[test]
|
||||
fn xmatrix_auth_pre_1_3() {
|
||||
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 key = "ed25519:key1".try_into().unwrap();
|
||||
let sig = "ABCDEF...".to_owned();
|
||||
let credentials: XMatrix = Credentials::decode(&header).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);
|
||||
@ -261,17 +256,20 @@ mod tests {
|
||||
|
||||
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]
|
||||
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 destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap();
|
||||
let key = "ed25519:key1".try_into().unwrap();
|
||||
let sig = "ABCDEF...".to_owned();
|
||||
let credentials: XMatrix = Credentials::decode(&header).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, Some(destination.clone()));
|
||||
assert_eq!(credentials.key, key);
|
||||
@ -279,6 +277,41 @@ mod tests {
|
||||
|
||||
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-msc2666 = ["ruma-client-api?/unstable-msc2666"]
|
||||
unstable-msc2705 = ["ruma-client-api?/unstable-msc2705"]
|
||||
unstable-msc2747 = ["ruma-events?/unstable-msc2747"]
|
||||
unstable-msc2867 = ["ruma-events?/unstable-msc2867"]
|
||||
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-msc3246 = ["ruma-events?/unstable-msc3246"]
|
||||
unstable-msc3266 = ["ruma-client-api?/unstable-msc3266"]
|
||||
unstable-msc3291 = ["ruma-events?/unstable-msc3291"]
|
||||
unstable-msc3381 = ["ruma-events?/unstable-msc3381"]
|
||||
unstable-msc3401 = ["ruma-events?/unstable-msc3401"]
|
||||
unstable-msc3488 = ["ruma-client-api?/unstable-msc3488", "ruma-events?/unstable-msc3488"]
|
||||
unstable-msc3489 = ["ruma-events?/unstable-msc3489"]
|
||||
unstable-msc3551 = ["ruma-events?/unstable-msc3551"]
|
||||
unstable-msc3552 = ["ruma-events?/unstable-msc3552"]
|
||||
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-msc3814 = ["ruma-client-api?/unstable-msc3814"]
|
||||
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-msc3930 = ["ruma-common/unstable-msc3930"]
|
||||
unstable-msc3931 = ["ruma-common/unstable-msc3931"]
|
||||
@ -268,6 +266,7 @@ unstable-msc4075 = ["ruma-events?/unstable-msc4075"]
|
||||
unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"]
|
||||
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
|
||||
unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"]
|
||||
unstable-msc4140 = ["ruma-client-api?/unstable-msc4140"]
|
||||
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
||||
unstable-unspecified = [
|
||||
"ruma-common/unstable-unspecified",
|
||||
@ -285,7 +284,6 @@ __ci = [
|
||||
"unstable-msc2448",
|
||||
"unstable-msc2654",
|
||||
"unstable-msc2666",
|
||||
"unstable-msc2705",
|
||||
"unstable-msc2747",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc2870",
|
||||
@ -297,10 +295,10 @@ __ci = [
|
||||
"unstable-msc3245-v1-compat",
|
||||
"unstable-msc3246",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc3291",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc3551",
|
||||
"unstable-msc3552",
|
||||
"unstable-msc3553",
|
||||
@ -310,7 +308,6 @@ __ci = [
|
||||
"unstable-msc3723",
|
||||
"unstable-msc3814",
|
||||
"unstable-msc3843",
|
||||
"unstable-msc3916",
|
||||
"unstable-msc3927",
|
||||
"unstable-msc3930",
|
||||
"unstable-msc3931",
|
||||
@ -323,6 +320,7 @@ __ci = [
|
||||
"unstable-msc4108",
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4140"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
|
@ -17,7 +17,7 @@ const OLD_URL_WHITELIST: &[&str] =
|
||||
|
||||
/// Authorized versions in URLs pointing to the new specs.
|
||||
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",
|
||||
// This should only be enabled if a legitimate use case is found.
|
||||
// "unstable",
|
||||
@ -220,48 +220,72 @@ fn get_page_ids(url: &str) -> Result<HashMap<String, HasDuplicates>> {
|
||||
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) =
|
||||
tag.attributes.get(b"id".as_slice()).and_then(|s| String::from_utf8(s.0.clone()).ok())
|
||||
else {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
/// are not unique in the source.
|
||||
///
|
||||
/// This is a reimplementation of the algorithm used for the spec.
|
||||
///
|
||||
/// See <https://github.com/matrix-org/matrix-spec/blob/6b02e393082570db2d0a651ddb79a365bc4a0f8d/static/js/toc.js#L25-L37>.
|
||||
fn uniquify_heading_id(
|
||||
mut id: String,
|
||||
/// This check is necessary because duplicates IDs have a number depending on their occurrence in a
|
||||
/// HTML page. If a duplicate ID is added, moved or removed from the spec, its number might change
|
||||
/// from one version to the next.
|
||||
fn heading_id_has_duplicates(
|
||||
id: &str,
|
||||
unique_ids: &mut HashMap<String, HasDuplicates>,
|
||||
) -> (String, HasDuplicates) {
|
||||
let base_id = id.clone();
|
||||
let mut counter: u16 = 0;
|
||||
let mut has_duplicates = HasDuplicates::No;
|
||||
) -> HasDuplicates {
|
||||
// IDs that should be duplicates end with `-{number}`.
|
||||
let Some((start, _end)) =
|
||||
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) {
|
||||
has_duplicates = HasDuplicates::Yes;
|
||||
// Update the first duplicate ID, because it doesn't end with a number.
|
||||
if let Some(other_id_has_dup) = unique_ids.get_mut(start) {
|
||||
*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) {
|
||||
println!(
|
||||
"\n{error}\nfile: {}:{}\nlink: {}",
|
||||
"\n{error}\n file: {}:{}\n link: {}",
|
||||
link.path.display(),
|
||||
link.line,
|
||||
link.url.get(..80).unwrap_or(&link.url),
|
||||
|
Loading…
x
Reference in New Issue
Block a user