Merge remote-tracking branch 'upstream/main' into conduwuit-changes
This commit is contained in:
commit
9a5bfad849
@ -137,6 +137,13 @@ use super::MyType;
|
|||||||
|
|
||||||
### Commit Messages
|
### Commit Messages
|
||||||
|
|
||||||
|
The commit message should start with the _area_ that is affected by the change.
|
||||||
|
An area is usually the name of the affected crate without the `ruma-` prefix,
|
||||||
|
except for the ruma-common crate, where the area is usually the name of the
|
||||||
|
top-level module, like `api` or `identifiers`. For example, the description of
|
||||||
|
a commit that affects the ruma-events crate should look like
|
||||||
|
"events: Add new event".
|
||||||
|
|
||||||
Write commit messages using the imperative mood, as if completing the sentence:
|
Write commit messages using the imperative mood, as if completing the sentence:
|
||||||
"If applied, this commit will \_\_\_." For example, use "Fix some bug" instead
|
"If applied, this commit will \_\_\_." For example, use "Fix some bug" instead
|
||||||
of "Fixed some bug" or "Add a feature" instead of "Added a feature".
|
of "Fixed some bug" or "Add a feature" instead of "Added a feature".
|
||||||
|
@ -33,7 +33,7 @@ pub mod v1 {
|
|||||||
|
|
||||||
/// One or more custom fields to help identify the third party location.
|
/// One or more custom fields to help identify the third party location.
|
||||||
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
||||||
#[ruma_api(query_map)]
|
#[ruma_api(query_all)]
|
||||||
pub fields: BTreeMap<String, String>,
|
pub fields: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ pub mod v1 {
|
|||||||
|
|
||||||
/// One or more custom fields that are passed to the AS to help identify the user.
|
/// One or more custom fields that are passed to the AS to help identify the user.
|
||||||
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
||||||
#[ruma_api(query_map)]
|
#[ruma_api(query_all)]
|
||||||
pub fields: BTreeMap<String, String>,
|
pub fields: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ Breaking changes:
|
|||||||
as before.
|
as before.
|
||||||
- Change type of `client_secret` field in `ThirdpartyIdCredentials`
|
- Change type of `client_secret` field in `ThirdpartyIdCredentials`
|
||||||
from `Box<ClientSecret>` to `OwnedClientSecret`
|
from `Box<ClientSecret>` to `OwnedClientSecret`
|
||||||
|
- Make `id_server` and `id_access_token` in `ThirdpartyIdCredentials` optional
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
@ -13,15 +14,23 @@ Improvements:
|
|||||||
- Heroes in `sync::sync_events::v4`: `SyncRequestList` and `RoomSubscription`
|
- Heroes in `sync::sync_events::v4`: `SyncRequestList` and `RoomSubscription`
|
||||||
both have a new `include_heroes` field. `SlidingSyncRoom` has a new `heroes`
|
both have a new `include_heroes` field. `SlidingSyncRoom` has a new `heroes`
|
||||||
field, with a new type `SlidingSyncRoomHero`.
|
field, with a new type `SlidingSyncRoomHero`.
|
||||||
- Add unstable support for authenticated media endpoints, according to MSC3916.
|
- Add support for authenticated media endpoints, according to MSC3916 / Matrix
|
||||||
|
1.11.
|
||||||
Bug fixes:
|
- They replace the newly deprecated `media::get_*` endpoints.
|
||||||
|
- Stabilize support for animated thumbnails, according to Matrix 1.11
|
||||||
- Rename `avatar` to `avatar_url` when (De)serializing
|
- Add support for terms of service at registration, according to MSC1692 /
|
||||||
|
Matrix 1.11
|
||||||
|
- Add unstable support for [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)
|
||||||
|
to send `Future` events and update `Future` events with `future_tokens`.
|
||||||
|
(`Future` events are scheduled messages that can be controlled
|
||||||
|
with `future_tokens` to send on demand or restart the timeout)
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
|
- Rename `avatar` to `avatar_url` when (De)serializing `SlidingSyncRoomHero`
|
||||||
- `user_id` of `SlidingSyncRoomHero` is now mandatory
|
- `user_id` of `SlidingSyncRoomHero` is now mandatory
|
||||||
|
- Make authentication with access token optional for the `change_password` and
|
||||||
|
`deactivate` endpoints.
|
||||||
|
|
||||||
# 0.18.0
|
# 0.18.0
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ unstable-exhaustive-types = ["ruma-common/unstable-exhaustive-types"]
|
|||||||
unstable-msc2666 = []
|
unstable-msc2666 = []
|
||||||
unstable-msc2448 = []
|
unstable-msc2448 = []
|
||||||
unstable-msc2654 = []
|
unstable-msc2654 = []
|
||||||
unstable-msc2705 = []
|
|
||||||
unstable-msc2965 = []
|
unstable-msc2965 = []
|
||||||
unstable-msc2967 = []
|
unstable-msc2967 = []
|
||||||
unstable-msc3266 = []
|
unstable-msc3266 = []
|
||||||
@ -48,10 +47,10 @@ unstable-msc3488 = []
|
|||||||
unstable-msc3575 = []
|
unstable-msc3575 = []
|
||||||
unstable-msc3814 = []
|
unstable-msc3814 = []
|
||||||
unstable-msc3843 = []
|
unstable-msc3843 = []
|
||||||
unstable-msc3916 = []
|
|
||||||
unstable-msc3983 = []
|
unstable-msc3983 = []
|
||||||
unstable-msc4108 = []
|
unstable-msc4108 = []
|
||||||
unstable-msc4121 = []
|
unstable-msc4121 = []
|
||||||
|
unstable-msc4140 = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
as_variant = { workspace = true }
|
as_variant = { workspace = true }
|
||||||
|
@ -17,7 +17,7 @@ pub mod v3 {
|
|||||||
const METADATA: Metadata = metadata! {
|
const METADATA: Metadata = metadata! {
|
||||||
method: POST,
|
method: POST,
|
||||||
rate_limited: true,
|
rate_limited: true,
|
||||||
authentication: AccessToken,
|
authentication: AccessTokenOptional,
|
||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/client/r0/account/password",
|
1.0 => "/_matrix/client/r0/account/password",
|
||||||
1.1 => "/_matrix/client/v3/account/password",
|
1.1 => "/_matrix/client/v3/account/password",
|
||||||
|
@ -20,7 +20,7 @@ pub mod v3 {
|
|||||||
const METADATA: Metadata = metadata! {
|
const METADATA: Metadata = metadata! {
|
||||||
method: POST,
|
method: POST,
|
||||||
rate_limited: true,
|
rate_limited: true,
|
||||||
authentication: AccessToken,
|
authentication: AccessTokenOptional,
|
||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/client/r0/account/deactivate",
|
1.0 => "/_matrix/client/r0/account/deactivate",
|
||||||
1.1 => "/_matrix/client/v3/account/deactivate",
|
1.1 => "/_matrix/client/v3/account/deactivate",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! Authenticated endpoints for the media repository, according to [MSC3916].
|
//! Authenticated endpoints for the [content repository].
|
||||||
//!
|
//!
|
||||||
//! [MSC3916]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
|
//! [content repository]: https://spec.matrix.org/latest/client-server-api/#content-repository
|
||||||
|
|
||||||
pub mod get_content;
|
pub mod get_content;
|
||||||
pub mod get_content_as_filename;
|
pub mod get_content_as_filename;
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Retrieve content from the media store.
|
//! Retrieve content from the media store.
|
||||||
|
|
||||||
pub mod unstable {
|
pub mod v1 {
|
||||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
//! `/v1/` ([spec])
|
||||||
//!
|
//!
|
||||||
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
|
//! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediadownloadservernamemediaid
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -17,10 +17,11 @@ pub mod unstable {
|
|||||||
|
|
||||||
const METADATA: Metadata = metadata! {
|
const METADATA: Metadata = metadata! {
|
||||||
method: GET,
|
method: GET,
|
||||||
rate_limited: false,
|
rate_limited: true,
|
||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id",
|
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id",
|
||||||
|
1.11 => "/_matrix/client/v1/media/download/:server_name/:media_id",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Retrieve content from the media store, specifying a filename to return.
|
//! Retrieve content from the media store, specifying a filename to return.
|
||||||
|
|
||||||
pub mod unstable {
|
pub mod v1 {
|
||||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
//! `/v1/` ([spec])
|
||||||
//!
|
//!
|
||||||
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
|
//! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediadownloadservernamemediaidfilename
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -17,10 +17,11 @@ pub mod unstable {
|
|||||||
|
|
||||||
const METADATA: Metadata = metadata! {
|
const METADATA: Metadata = metadata! {
|
||||||
method: GET,
|
method: GET,
|
||||||
rate_limited: false,
|
rate_limited: true,
|
||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id/:filename",
|
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/download/:server_name/:media_id/:filename",
|
||||||
|
1.11 => "/_matrix/client/v1/media/download/:server_name/:media_id/:filename",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Get a thumbnail of content from the media store.
|
//! Get a thumbnail of content from the media store.
|
||||||
|
|
||||||
pub mod unstable {
|
pub mod v1 {
|
||||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
//! `/v1/` ([spec])
|
||||||
//!
|
//!
|
||||||
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
|
//! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediathumbnailservernamemediaid
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ pub mod unstable {
|
|||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/:server_name/:media_id",
|
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/:server_name/:media_id",
|
||||||
|
1.11 => "/_matrix/client/v1/media/thumbnail/:server_name/:media_id",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,18 +70,12 @@ pub mod unstable {
|
|||||||
|
|
||||||
/// Whether the server should return an animated thumbnail.
|
/// Whether the server should return an animated thumbnail.
|
||||||
///
|
///
|
||||||
/// When `true`, the server should return an animated thumbnail if possible and supported.
|
/// When `Some(true)`, the server should return an animated thumbnail if possible and
|
||||||
/// Otherwise it must not return an animated thumbnail.
|
/// supported. When `Some(false)`, the server must not return an animated
|
||||||
///
|
/// thumbnail. When `None`, the server should not return an animated thumbnail.
|
||||||
/// Defaults to `false`.
|
|
||||||
#[cfg(feature = "unstable-msc2705")]
|
|
||||||
#[ruma_api(query)]
|
#[ruma_api(query)]
|
||||||
#[serde(
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
rename = "org.matrix.msc2705.animated",
|
pub animated: Option<bool>,
|
||||||
default,
|
|
||||||
skip_serializing_if = "ruma_common::serde::is_default"
|
|
||||||
)]
|
|
||||||
pub animated: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response type for the `get_content_thumbnail` endpoint.
|
/// Response type for the `get_content_thumbnail` endpoint.
|
||||||
@ -111,8 +106,7 @@ pub mod unstable {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
timeout_ms: crate::media::default_download_timeout(),
|
timeout_ms: crate::media::default_download_timeout(),
|
||||||
#[cfg(feature = "unstable-msc2705")]
|
animated: None,
|
||||||
animated: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Gets the config for the media repository.
|
//! Gets the config for the media repository.
|
||||||
|
|
||||||
pub mod unstable {
|
pub mod v1 {
|
||||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
//! `/v1/` ([spec])
|
||||||
//!
|
//!
|
||||||
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
|
//! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediaconfig
|
||||||
|
|
||||||
use js_int::UInt;
|
use js_int::UInt;
|
||||||
use ruma_common::{
|
use ruma_common::{
|
||||||
@ -19,6 +19,7 @@ pub mod unstable {
|
|||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/config",
|
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/config",
|
||||||
|
1.11 => "/_matrix/client/v1/media/config",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Get a preview for a URL.
|
//! Get a preview for a URL.
|
||||||
|
|
||||||
pub mod unstable {
|
pub mod v1 {
|
||||||
//! `/unstable/org.matrix.msc3916/` ([MSC])
|
//! `/v1/` ([spec])
|
||||||
//!
|
//!
|
||||||
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
|
//! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediapreview_url
|
||||||
|
|
||||||
use ruma_common::{
|
use ruma_common::{
|
||||||
api::{request, response, Metadata},
|
api::{request, response, Metadata},
|
||||||
@ -20,6 +20,7 @@ pub mod unstable {
|
|||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url",
|
unstable => "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url",
|
||||||
|
1.11 => "/_matrix/client/v1/media/preview_url",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
38
crates/ruma-client-api/src/future.rs
Normal file
38
crates/ruma-client-api/src/future.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
//! Endpoints for sending and receiving futures
|
||||||
|
|
||||||
|
pub mod send_future_message_event;
|
||||||
|
pub mod send_future_state_event;
|
||||||
|
pub mod update_future;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use web_time::Duration;
|
||||||
|
|
||||||
|
/// The query parameters for a future request.
|
||||||
|
/// It can contain the possible timeout and the future_group_id combinations.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum FutureParameters {
|
||||||
|
/// Only sending the timeout creates a timeout future with a new (server generated)
|
||||||
|
/// group id. The optional group id is used to create a secondary timeout.
|
||||||
|
/// In a future group with two timeouts only one of them will ever be sent.
|
||||||
|
Timeout {
|
||||||
|
/// The timeout duration for this Future.
|
||||||
|
#[serde(with = "ruma_common::serde::duration::ms")]
|
||||||
|
#[serde(rename = "future_timeout")]
|
||||||
|
timeout: Duration,
|
||||||
|
/// The associated group for this Future.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "future_group_id")]
|
||||||
|
group_id: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Adds an additional action to a future without a timeout but requires a future group_id.
|
||||||
|
/// A possible matrix event that this future group can resolve to. It can be sent by using the
|
||||||
|
/// send_token as an alternative to the timeout future of an already existing group.
|
||||||
|
Action {
|
||||||
|
/// The associated group for this Future.
|
||||||
|
#[serde(rename = "future_group_id")]
|
||||||
|
group_id: String,
|
||||||
|
},
|
||||||
|
}
|
182
crates/ruma-client-api/src/future/send_future_message_event.rs
Normal file
182
crates/ruma-client-api/src/future/send_future_message_event.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
//! `PUT /_matrix/client/*/rooms/{roomId}/send_future/{eventType}/{txnId}`
|
||||||
|
//!
|
||||||
|
//! Send a future (a scheduled message) to a room. [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)
|
||||||
|
|
||||||
|
pub mod unstable {
|
||||||
|
//! `msc4140` ([MSC])
|
||||||
|
//!
|
||||||
|
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
|
||||||
|
|
||||||
|
use ruma_common::{
|
||||||
|
api::{request, response, Metadata},
|
||||||
|
metadata,
|
||||||
|
serde::Raw,
|
||||||
|
OwnedRoomId, OwnedTransactionId,
|
||||||
|
};
|
||||||
|
use ruma_events::{AnyMessageLikeEventContent, MessageLikeEventContent, MessageLikeEventType};
|
||||||
|
use serde_json::value::to_raw_value as to_raw_json_value;
|
||||||
|
|
||||||
|
use crate::future::FutureParameters;
|
||||||
|
|
||||||
|
const METADATA: Metadata = metadata! {
|
||||||
|
method: PUT,
|
||||||
|
rate_limited: false,
|
||||||
|
authentication: AccessToken,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/client/unstable/org.matrix.msc4140/rooms/:room_id/send_future/:event_type/:txn_id",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/// Request type for the [`send_future_message_event`](crate::future::send_future_message_event)
|
||||||
|
/// endpoint.
|
||||||
|
#[request(error = crate::Error)]
|
||||||
|
pub struct Request {
|
||||||
|
/// The room to send the event to.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub room_id: OwnedRoomId,
|
||||||
|
|
||||||
|
/// The type of event to send.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub event_type: MessageLikeEventType,
|
||||||
|
|
||||||
|
/// The transaction ID for this event.
|
||||||
|
///
|
||||||
|
/// Clients should generate a unique ID across requests within the
|
||||||
|
/// same session. A session is identified by an access token, and
|
||||||
|
/// persists when the [access token is refreshed].
|
||||||
|
///
|
||||||
|
/// It will be used by the server to ensure idempotency of requests.
|
||||||
|
///
|
||||||
|
/// [access token is refreshed]: https://spec.matrix.org/latest/client-server-api/#refreshing-access-tokens
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub txn_id: OwnedTransactionId,
|
||||||
|
|
||||||
|
/// Additional parameters to describe sending a future.
|
||||||
|
///
|
||||||
|
/// Only three combinations for `future_timeout` and `future_group_id` are possible.
|
||||||
|
/// The enum [`FutureParameters`] enforces this.
|
||||||
|
#[ruma_api(query_all)]
|
||||||
|
pub future_parameters: FutureParameters,
|
||||||
|
|
||||||
|
/// The event content to send.
|
||||||
|
#[ruma_api(body)]
|
||||||
|
pub body: Raw<AnyMessageLikeEventContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the
|
||||||
|
/// [`send_future_message_event`](crate::future::send_future_message_event) endpoint.
|
||||||
|
#[response(error = crate::Error)]
|
||||||
|
pub struct Response {
|
||||||
|
/// A token to send/insert the future into the DAG.
|
||||||
|
pub send_token: String,
|
||||||
|
/// A token to cancel this future. It will never be send if this is called.
|
||||||
|
pub cancel_token: String,
|
||||||
|
/// The `future_group_id` generated for this future. Used to connect multiple futures
|
||||||
|
/// only one of the connected futures will be sent and inserted into the DAG.
|
||||||
|
pub future_group_id: String,
|
||||||
|
/// A token used to refresh the timer of the future. This allows
|
||||||
|
/// to implement heartbeat like capabilities. An event is only sent once
|
||||||
|
/// a refresh in the timeout interval is missed.
|
||||||
|
///
|
||||||
|
/// If the future does not have a timeout this will be `None`.
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Creates a new `Request` with the given room id, transaction id future_parameters and
|
||||||
|
/// event content.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Since `Request` stores the request body in serialized form, this function can fail if
|
||||||
|
/// `T`s [`::serde::Serialize`] implementation can fail.
|
||||||
|
pub fn new<T>(
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
txn_id: OwnedTransactionId,
|
||||||
|
future_parameters: FutureParameters,
|
||||||
|
content: &T,
|
||||||
|
) -> serde_json::Result<Self>
|
||||||
|
where
|
||||||
|
T: MessageLikeEventContent,
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
room_id,
|
||||||
|
txn_id,
|
||||||
|
event_type: content.event_type(),
|
||||||
|
future_parameters,
|
||||||
|
body: Raw::from_json(to_raw_json_value(content)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Request` with the given room id, transaction id, event type,
|
||||||
|
/// future parameters and raw event content.
|
||||||
|
pub fn new_raw(
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
txn_id: OwnedTransactionId,
|
||||||
|
event_type: MessageLikeEventType,
|
||||||
|
future_parameters: FutureParameters,
|
||||||
|
body: Raw<AnyMessageLikeEventContent>,
|
||||||
|
) -> Self {
|
||||||
|
Self { room_id, event_type, txn_id, future_parameters, body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Creates a new `Response` with the tokens required to control the future using the
|
||||||
|
/// [`crate::future::update_future::unstable::Request`] request.
|
||||||
|
pub fn new(
|
||||||
|
send_token: String,
|
||||||
|
cancel_token: String,
|
||||||
|
future_group_id: String,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self { send_token, cancel_token, future_group_id, refresh_token }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "client"))]
|
||||||
|
mod tests {
|
||||||
|
use ruma_common::{
|
||||||
|
api::{MatrixVersion, OutgoingRequest, SendAccessToken},
|
||||||
|
owned_room_id,
|
||||||
|
};
|
||||||
|
use ruma_events::room::message::RoomMessageEventContent;
|
||||||
|
use serde_json::{json, Value as JsonValue};
|
||||||
|
use web_time::Duration;
|
||||||
|
|
||||||
|
use super::Request;
|
||||||
|
use crate::future::send_future_message_event::unstable::FutureParameters;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_message_future_request() {
|
||||||
|
let room_id = owned_room_id!("!roomid:example.org");
|
||||||
|
|
||||||
|
let req = Request::new(
|
||||||
|
room_id,
|
||||||
|
"1234".into(),
|
||||||
|
FutureParameters::Timeout {
|
||||||
|
timeout: Duration::from_millis(103),
|
||||||
|
group_id: Some("testId".to_owned()),
|
||||||
|
},
|
||||||
|
&RoomMessageEventContent::text_plain("test"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let request: http::Request<Vec<u8>> = req
|
||||||
|
.try_into_http_request(
|
||||||
|
"https://homeserver.tld",
|
||||||
|
SendAccessToken::IfRequired("auth_tok"),
|
||||||
|
&[MatrixVersion::V1_1],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let (parts, body) = request.into_parts();
|
||||||
|
assert_eq!(
|
||||||
|
"https://homeserver.tld/_matrix/client/unstable/org.matrix.msc4140/rooms/!roomid:example.org/send_future/m.room.message/1234?future_timeout=103&future_group_id=testId",
|
||||||
|
parts.uri.to_string()
|
||||||
|
);
|
||||||
|
assert_eq!("PUT", parts.method.to_string());
|
||||||
|
assert_eq!(
|
||||||
|
json!({"msgtype":"m.text","body":"test"}),
|
||||||
|
serde_json::from_str::<JsonValue>(std::str::from_utf8(&body).unwrap()).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
175
crates/ruma-client-api/src/future/send_future_state_event.rs
Normal file
175
crates/ruma-client-api/src/future/send_future_state_event.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
//! `PUT /_matrix/client/*/rooms/{roomId}/state_future/{eventType}/{txnId}`
|
||||||
|
//!
|
||||||
|
//! Send a future state (a scheduled state event) to a room. [MSC](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)
|
||||||
|
|
||||||
|
pub mod unstable {
|
||||||
|
//! `msc4140` ([MSC])
|
||||||
|
//!
|
||||||
|
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
|
||||||
|
|
||||||
|
use ruma_common::{
|
||||||
|
api::{request, response, Metadata},
|
||||||
|
metadata,
|
||||||
|
serde::Raw,
|
||||||
|
OwnedRoomId,
|
||||||
|
};
|
||||||
|
use ruma_events::{AnyStateEventContent, StateEventContent, StateEventType};
|
||||||
|
use serde_json::value::to_raw_value as to_raw_json_value;
|
||||||
|
|
||||||
|
use crate::future::FutureParameters;
|
||||||
|
|
||||||
|
const METADATA: Metadata = metadata! {
|
||||||
|
method: PUT,
|
||||||
|
rate_limited: false,
|
||||||
|
authentication: AccessToken,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/client/unstable/org.matrix.msc4140/rooms/:room_id/state_future/:event_type/:state_key",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Request type for the [`send_future_state_event`](crate::future::send_future_state_event)
|
||||||
|
/// endpoint.
|
||||||
|
#[request(error = crate::Error)]
|
||||||
|
pub struct Request {
|
||||||
|
/// The room to send the event to.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub room_id: OwnedRoomId,
|
||||||
|
|
||||||
|
/// The type of event to send.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub event_type: StateEventType,
|
||||||
|
|
||||||
|
/// The state_key for the state to send.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub state_key: String,
|
||||||
|
|
||||||
|
/// Additional parameters to describe sending a future.
|
||||||
|
///
|
||||||
|
/// Only three combinations for `future_timeout` and `future_group_id` are possible.
|
||||||
|
/// The enum [`FutureParameters`] enforces this.
|
||||||
|
#[ruma_api(query_all)]
|
||||||
|
pub future_parameters: FutureParameters,
|
||||||
|
|
||||||
|
/// The event content to send.
|
||||||
|
#[ruma_api(body)]
|
||||||
|
pub body: Raw<AnyStateEventContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the [`send_future_state_event`](crate::future::send_future_state_event)
|
||||||
|
/// endpoint.
|
||||||
|
#[response(error = crate::Error)]
|
||||||
|
pub struct Response {
|
||||||
|
/// A token to send/insert the future into the DAG.
|
||||||
|
pub send_token: String,
|
||||||
|
/// A token to cancel this future. It will never be send if this is called.
|
||||||
|
pub cancel_token: String,
|
||||||
|
/// The `future_group_id` generated for this future. Used to connect multiple futures
|
||||||
|
/// only one of the connected futures will be sent and inserted into the DAG.
|
||||||
|
pub future_group_id: String,
|
||||||
|
/// A token used to refresh the timer of the future. This allows
|
||||||
|
/// to implement heardbeat like capabilities. An event is only send once
|
||||||
|
/// a refresh in the timeout interval is missed.
|
||||||
|
///
|
||||||
|
/// If the future does not have a timeout this will be `None`.
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Creates a new `Request` with the given room id, state_key future_parameters and
|
||||||
|
/// event content.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Since `Request` stores the request body in serialized form, this function can fail if
|
||||||
|
/// `T`s [`::serde::Serialize`] implementation can fail.
|
||||||
|
pub fn new<T>(
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
state_key: String,
|
||||||
|
future_parameters: FutureParameters,
|
||||||
|
content: &T,
|
||||||
|
) -> serde_json::Result<Self>
|
||||||
|
where
|
||||||
|
T: StateEventContent,
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
room_id,
|
||||||
|
state_key,
|
||||||
|
event_type: content.event_type(),
|
||||||
|
future_parameters,
|
||||||
|
body: Raw::from_json(to_raw_json_value(content)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Request` with the given room id, event type, state key,
|
||||||
|
/// future parameters and raw event content.
|
||||||
|
pub fn new_raw(
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
state_key: String,
|
||||||
|
event_type: StateEventType,
|
||||||
|
future_parameters: FutureParameters,
|
||||||
|
body: Raw<AnyStateEventContent>,
|
||||||
|
) -> Self {
|
||||||
|
Self { room_id, event_type, state_key, body, future_parameters }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Creates a new `Response` with the tokens required to control the future using the
|
||||||
|
/// [`crate::future::update_future::unstable::Request`] request.
|
||||||
|
pub fn new(
|
||||||
|
send_token: String,
|
||||||
|
cancel_token: String,
|
||||||
|
future_group_id: String,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self { send_token, cancel_token, future_group_id, refresh_token }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "client"))]
|
||||||
|
mod tests {
|
||||||
|
use ruma_common::{
|
||||||
|
api::{MatrixVersion, OutgoingRequest, SendAccessToken},
|
||||||
|
owned_room_id,
|
||||||
|
};
|
||||||
|
use ruma_events::room::topic::RoomTopicEventContent;
|
||||||
|
use serde_json::{json, Value as JsonValue};
|
||||||
|
use web_time::Duration;
|
||||||
|
|
||||||
|
use super::Request;
|
||||||
|
use crate::future::FutureParameters;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_state_future_request() {
|
||||||
|
let room_id = owned_room_id!("!roomid:example.org");
|
||||||
|
|
||||||
|
let req = Request::new(
|
||||||
|
room_id,
|
||||||
|
"@userAsStateKey:example.org".to_owned(),
|
||||||
|
FutureParameters::Timeout {
|
||||||
|
timeout: Duration::from_millis(1_234_321),
|
||||||
|
group_id: Some("abs1abs1abs1abs1".to_owned()),
|
||||||
|
},
|
||||||
|
&RoomTopicEventContent::new("my_topic".to_owned()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let request: http::Request<Vec<u8>> = req
|
||||||
|
.try_into_http_request(
|
||||||
|
"https://homeserver.tld",
|
||||||
|
SendAccessToken::IfRequired("auth_tok"),
|
||||||
|
&[MatrixVersion::V1_1],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let (parts, body) = request.into_parts();
|
||||||
|
assert_eq!(
|
||||||
|
"https://homeserver.tld/_matrix/client/unstable/org.matrix.msc4140/rooms/!roomid:example.org/state_future/m.room.topic/@userAsStateKey:example.org?future_timeout=1234321&future_group_id=abs1abs1abs1abs1",
|
||||||
|
parts.uri.to_string()
|
||||||
|
);
|
||||||
|
assert_eq!("PUT", parts.method.to_string());
|
||||||
|
assert_eq!(
|
||||||
|
json!({"topic": "my_topic"}),
|
||||||
|
serde_json::from_str::<JsonValue>(std::str::from_utf8(&body).unwrap()).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
crates/ruma-client-api/src/future/update_future.rs
Normal file
49
crates/ruma-client-api/src/future/update_future.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//! `POST /_matrix/client/*/futures/{token}`
|
||||||
|
//!
|
||||||
|
//! Send a future token to update/cancel/send the associated future event.
|
||||||
|
|
||||||
|
pub mod unstable {
|
||||||
|
//! `msc3814` ([MSC])
|
||||||
|
//!
|
||||||
|
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
|
||||||
|
|
||||||
|
use ruma_common::{
|
||||||
|
api::{request, response, Metadata},
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
const METADATA: Metadata = metadata! {
|
||||||
|
method: POST,
|
||||||
|
rate_limited: true,
|
||||||
|
authentication: None,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/client/unstable/org.matrix.msc4140/future/:token",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Request type for the [`update_future`](crate::future::update_future) endpoint.
|
||||||
|
#[request(error = crate::Error)]
|
||||||
|
pub struct Request {
|
||||||
|
/// The token.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Creates a new `Request` to update a future. This is an unauthenticated request and only
|
||||||
|
/// requires the future token.
|
||||||
|
pub fn new(token: String) -> serde_json::Result<Self> {
|
||||||
|
Ok(Self { token })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the [`update_future`](crate::future::update_future) endpoint.
|
||||||
|
#[response(error = crate::Error)]
|
||||||
|
pub struct Response {}
|
||||||
|
impl Response {
|
||||||
|
/// Creates a new response for the [`update_future`](crate::future::update_future) endpoint.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Response {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@
|
|||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod alias;
|
pub mod alias;
|
||||||
pub mod appservice;
|
pub mod appservice;
|
||||||
#[cfg(feature = "unstable-msc3916")]
|
|
||||||
pub mod authenticated_media;
|
pub mod authenticated_media;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
@ -24,6 +23,8 @@ pub mod directory;
|
|||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
|
#[cfg(feature = "unstable-msc4140")]
|
||||||
|
pub mod future;
|
||||||
pub mod http_headers;
|
pub mod http_headers;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod knock;
|
pub mod knock;
|
||||||
|
@ -24,11 +24,16 @@ pub mod v3 {
|
|||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/media/r0/download/:server_name/:media_id",
|
1.0 => "/_matrix/media/r0/download/:server_name/:media_id",
|
||||||
1.1 => "/_matrix/media/v3/download/:server_name/:media_id",
|
1.1 => "/_matrix/media/v3/download/:server_name/:media_id",
|
||||||
|
1.11 => deprecated,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Request type for the `get_media_content` endpoint.
|
/// Request type for the `get_media_content` endpoint.
|
||||||
#[request(error = crate::Error)]
|
#[request(error = crate::Error)]
|
||||||
|
#[deprecated = "\
|
||||||
|
Since Matrix 1.11, clients should use `authenticated_media::get_content::v1::Request` \
|
||||||
|
instead if the homeserver supports it.\
|
||||||
|
"]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// The server name from the mxc:// URI (the authoritory component).
|
/// The server name from the mxc:// URI (the authoritory component).
|
||||||
#[ruma_api(path)]
|
#[ruma_api(path)]
|
||||||
@ -106,6 +111,7 @@ pub mod v3 {
|
|||||||
pub cache_control: Option<String>,
|
pub cache_control: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
impl Request {
|
impl Request {
|
||||||
/// Creates a new `Request` with the given media ID and server name.
|
/// Creates a new `Request` with the given media ID and server name.
|
||||||
pub fn new(media_id: String, server_name: OwnedServerName) -> Self {
|
pub fn new(media_id: String, server_name: OwnedServerName) -> Self {
|
||||||
|
@ -24,11 +24,16 @@ pub mod v3 {
|
|||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/media/r0/download/:server_name/:media_id/:filename",
|
1.0 => "/_matrix/media/r0/download/:server_name/:media_id/:filename",
|
||||||
1.1 => "/_matrix/media/v3/download/:server_name/:media_id/:filename",
|
1.1 => "/_matrix/media/v3/download/:server_name/:media_id/:filename",
|
||||||
|
1.11 => deprecated,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Request type for the `get_media_content_as_filename` endpoint.
|
/// Request type for the `get_media_content_as_filename` endpoint.
|
||||||
#[request(error = crate::Error)]
|
#[request(error = crate::Error)]
|
||||||
|
#[deprecated = "\
|
||||||
|
Since Matrix 1.11, clients should use `authenticated_media::get_content_as_filename::v1::Request` \
|
||||||
|
instead if the homeserver supports it.\
|
||||||
|
"]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// The server name from the mxc:// URI (the authoritory component).
|
/// The server name from the mxc:// URI (the authoritory component).
|
||||||
#[ruma_api(path)]
|
#[ruma_api(path)]
|
||||||
@ -110,6 +115,7 @@ pub mod v3 {
|
|||||||
pub cache_control: Option<String>,
|
pub cache_control: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
impl Request {
|
impl Request {
|
||||||
/// Creates a new `Request` with the given media ID, server name and filename.
|
/// Creates a new `Request` with the given media ID, server name and filename.
|
||||||
pub fn new(media_id: String, server_name: OwnedServerName, filename: String) -> Self {
|
pub fn new(media_id: String, server_name: OwnedServerName, filename: String) -> Self {
|
||||||
|
@ -27,11 +27,16 @@ pub mod v3 {
|
|||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/media/r0/thumbnail/:server_name/:media_id",
|
1.0 => "/_matrix/media/r0/thumbnail/:server_name/:media_id",
|
||||||
1.1 => "/_matrix/media/v3/thumbnail/:server_name/:media_id",
|
1.1 => "/_matrix/media/v3/thumbnail/:server_name/:media_id",
|
||||||
|
1.11 => deprecated,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Request type for the `get_content_thumbnail` endpoint.
|
/// Request type for the `get_content_thumbnail` endpoint.
|
||||||
#[request(error = crate::Error)]
|
#[request(error = crate::Error)]
|
||||||
|
#[deprecated = "\
|
||||||
|
Since Matrix 1.11, clients should use `authenticated_media::get_content_thumbnail::v1::Request` \
|
||||||
|
instead if the homeserver supports it.\
|
||||||
|
"]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// The server name from the mxc:// URI (the authoritory component).
|
/// The server name from the mxc:// URI (the authoritory component).
|
||||||
#[ruma_api(path)]
|
#[ruma_api(path)]
|
||||||
@ -90,18 +95,12 @@ pub mod v3 {
|
|||||||
|
|
||||||
/// Whether the server should return an animated thumbnail.
|
/// Whether the server should return an animated thumbnail.
|
||||||
///
|
///
|
||||||
/// When `true`, the server should return an animated thumbnail if possible and supported.
|
/// When `Some(true)`, the server should return an animated thumbnail if possible and
|
||||||
/// Otherwise it must not return an animated thumbnail.
|
/// supported. When `Some(false)`, the server must not return an animated
|
||||||
///
|
/// thumbnail. When `None`, the server should not return an animated thumbnail.
|
||||||
/// Defaults to `false`.
|
|
||||||
#[cfg(feature = "unstable-msc2705")]
|
|
||||||
#[ruma_api(query)]
|
#[ruma_api(query)]
|
||||||
#[serde(
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
rename = "org.matrix.msc2705.animated",
|
pub animated: Option<bool>,
|
||||||
default,
|
|
||||||
skip_serializing_if = "ruma_common::serde::is_default"
|
|
||||||
)]
|
|
||||||
pub animated: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response type for the `get_content_thumbnail` endpoint.
|
/// Response type for the `get_content_thumbnail` endpoint.
|
||||||
@ -141,6 +140,7 @@ pub mod v3 {
|
|||||||
pub content_disposition: Option<String>,
|
pub content_disposition: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
impl Request {
|
impl Request {
|
||||||
/// Creates a new `Request` with the given media ID, server name, desired thumbnail width
|
/// Creates a new `Request` with the given media ID, server name, desired thumbnail width
|
||||||
/// and desired thumbnail height.
|
/// and desired thumbnail height.
|
||||||
@ -159,8 +159,7 @@ pub mod v3 {
|
|||||||
allow_remote: true,
|
allow_remote: true,
|
||||||
timeout_ms: crate::media::default_download_timeout(),
|
timeout_ms: crate::media::default_download_timeout(),
|
||||||
allow_redirect: false,
|
allow_redirect: false,
|
||||||
#[cfg(feature = "unstable-msc2705")]
|
animated: None,
|
||||||
animated: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,12 +20,17 @@ pub mod v3 {
|
|||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/media/r0/config",
|
1.0 => "/_matrix/media/r0/config",
|
||||||
1.1 => "/_matrix/media/v3/config",
|
1.1 => "/_matrix/media/v3/config",
|
||||||
|
1.11 => deprecated,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Request type for the `get_media_config` endpoint.
|
/// Request type for the `get_media_config` endpoint.
|
||||||
#[request(error = crate::Error)]
|
#[request(error = crate::Error)]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
#[deprecated = "\
|
||||||
|
Since Matrix 1.11, clients should use `authenticated_media::get_media_config::v1::Request` \
|
||||||
|
instead if the homeserver supports it.\
|
||||||
|
"]
|
||||||
pub struct Request {}
|
pub struct Request {}
|
||||||
|
|
||||||
/// Response type for the `get_media_config` endpoint.
|
/// Response type for the `get_media_config` endpoint.
|
||||||
@ -36,6 +41,7 @@ pub mod v3 {
|
|||||||
pub upload_size: UInt,
|
pub upload_size: UInt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
impl Request {
|
impl Request {
|
||||||
/// Creates an empty `Request`.
|
/// Creates an empty `Request`.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -21,11 +21,16 @@ pub mod v3 {
|
|||||||
history: {
|
history: {
|
||||||
1.0 => "/_matrix/media/r0/preview_url",
|
1.0 => "/_matrix/media/r0/preview_url",
|
||||||
1.1 => "/_matrix/media/v3/preview_url",
|
1.1 => "/_matrix/media/v3/preview_url",
|
||||||
|
1.11 => deprecated,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Request type for the `get_media_preview` endpoint.
|
/// Request type for the `get_media_preview` endpoint.
|
||||||
#[request(error = crate::Error)]
|
#[request(error = crate::Error)]
|
||||||
|
#[deprecated = "\
|
||||||
|
Since Matrix 1.11, clients should use `authenticated_media::get_media_preview::v1::Request` \
|
||||||
|
instead if the homeserver supports it.\
|
||||||
|
"]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// URL to get a preview of.
|
/// URL to get a preview of.
|
||||||
#[ruma_api(query)]
|
#[ruma_api(query)]
|
||||||
@ -49,6 +54,7 @@ pub mod v3 {
|
|||||||
pub data: Option<Box<RawJsonValue>>,
|
pub data: Option<Box<RawJsonValue>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
impl Request {
|
impl Request {
|
||||||
/// Creates a new `Request` with the given url.
|
/// Creates a new `Request` with the given url.
|
||||||
pub fn new(url: String) -> Self {
|
pub fn new(url: String) -> Self {
|
||||||
|
@ -9,8 +9,8 @@ pub mod v3 {
|
|||||||
//! [by their Matrix identifier][spec-mxid], and one to invite a user
|
//! [by their Matrix identifier][spec-mxid], and one to invite a user
|
||||||
//! [by their third party identifier][spec-3pid].
|
//! [by their third party identifier][spec-3pid].
|
||||||
//!
|
//!
|
||||||
//! [spec-mxid]: https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3roomsroomidinvite
|
//! [spec-mxid]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3roomsroomidinvite
|
||||||
//! [spec-3pid]: https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3roomsroomidinvite-1
|
//! [spec-3pid]: https://spec.matrix.org/latest/client-server-api/#thirdparty_post_matrixclientv3roomsroomidinvite
|
||||||
|
|
||||||
use ruma_common::{
|
use ruma_common::{
|
||||||
api::{request, response, Metadata},
|
api::{request, response, Metadata},
|
||||||
|
@ -34,7 +34,7 @@ pub mod v3 {
|
|||||||
|
|
||||||
/// One or more custom fields to help identify the third party location.
|
/// One or more custom fields to help identify the third party location.
|
||||||
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
||||||
#[ruma_api(query_map)]
|
#[ruma_api(query_all)]
|
||||||
pub fields: BTreeMap<String, String>,
|
pub fields: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ pub mod v3 {
|
|||||||
|
|
||||||
/// One or more custom fields that are passed to the AS to help identify the user.
|
/// One or more custom fields that are passed to the AS to help identify the user.
|
||||||
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
// The specification is incorrect for this parameter. See [matrix-spec#560](https://github.com/matrix-org/matrix-spec/issues/560).
|
||||||
#[ruma_api(query_map)]
|
#[ruma_api(query_all)]
|
||||||
pub fields: BTreeMap<String, String>,
|
pub fields: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,11 @@ pub enum AuthData {
|
|||||||
/// Fallback acknowledgement.
|
/// Fallback acknowledgement.
|
||||||
FallbackAcknowledgement(FallbackAcknowledgement),
|
FallbackAcknowledgement(FallbackAcknowledgement),
|
||||||
|
|
||||||
|
/// Terms of service (`m.login.terms`).
|
||||||
|
///
|
||||||
|
/// This type is only valid during account registration.
|
||||||
|
Terms(Terms),
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
_Custom(CustomAuthData),
|
_Custom(CustomAuthData),
|
||||||
}
|
}
|
||||||
@ -90,6 +95,7 @@ impl AuthData {
|
|||||||
"m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?),
|
"m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?),
|
||||||
"m.login.dummy" => Self::Dummy(deserialize_variant(session, data)?),
|
"m.login.dummy" => Self::Dummy(deserialize_variant(session, data)?),
|
||||||
"m.registration_token" => Self::RegistrationToken(deserialize_variant(session, data)?),
|
"m.registration_token" => Self::RegistrationToken(deserialize_variant(session, data)?),
|
||||||
|
"m.login.terms" => Self::Terms(deserialize_variant(session, data)?),
|
||||||
_ => {
|
_ => {
|
||||||
Self::_Custom(CustomAuthData { auth_type: auth_type.into(), session, extra: data })
|
Self::_Custom(CustomAuthData { auth_type: auth_type.into(), session, extra: data })
|
||||||
}
|
}
|
||||||
@ -111,6 +117,7 @@ impl AuthData {
|
|||||||
Self::Dummy(_) => Some(AuthType::Dummy),
|
Self::Dummy(_) => Some(AuthType::Dummy),
|
||||||
Self::RegistrationToken(_) => Some(AuthType::RegistrationToken),
|
Self::RegistrationToken(_) => Some(AuthType::RegistrationToken),
|
||||||
Self::FallbackAcknowledgement(_) => None,
|
Self::FallbackAcknowledgement(_) => None,
|
||||||
|
Self::Terms(_) => Some(AuthType::Terms),
|
||||||
Self::_Custom(c) => Some(AuthType::_Custom(PrivOwnedStr(c.auth_type.as_str().into()))),
|
Self::_Custom(c) => Some(AuthType::_Custom(PrivOwnedStr(c.auth_type.as_str().into()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,6 +132,7 @@ impl AuthData {
|
|||||||
Self::Dummy(x) => x.session.as_deref(),
|
Self::Dummy(x) => x.session.as_deref(),
|
||||||
Self::RegistrationToken(x) => x.session.as_deref(),
|
Self::RegistrationToken(x) => x.session.as_deref(),
|
||||||
Self::FallbackAcknowledgement(x) => Some(&x.session),
|
Self::FallbackAcknowledgement(x) => Some(&x.session),
|
||||||
|
Self::Terms(x) => x.session.as_deref(),
|
||||||
Self::_Custom(x) => x.session.as_deref(),
|
Self::_Custom(x) => x.session.as_deref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,8 +173,10 @@ impl AuthData {
|
|||||||
Self::RegistrationToken(x) => {
|
Self::RegistrationToken(x) => {
|
||||||
Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None }))
|
Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None }))
|
||||||
}
|
}
|
||||||
// Dummy and fallback acknowledgement have no associated data
|
// Dummy, fallback acknowledgement, and terms of service have no associated data
|
||||||
Self::Dummy(_) | Self::FallbackAcknowledgement(_) => Cow::Owned(JsonObject::default()),
|
Self::Dummy(_) | Self::FallbackAcknowledgement(_) | Self::Terms(_) => {
|
||||||
|
Cow::Owned(JsonObject::default())
|
||||||
|
}
|
||||||
Self::_Custom(c) => Cow::Borrowed(&c.extra),
|
Self::_Custom(c) => Cow::Borrowed(&c.extra),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,6 +193,7 @@ impl fmt::Debug for AuthData {
|
|||||||
Self::Dummy(inner) => inner.fmt(f),
|
Self::Dummy(inner) => inner.fmt(f),
|
||||||
Self::RegistrationToken(inner) => inner.fmt(f),
|
Self::RegistrationToken(inner) => inner.fmt(f),
|
||||||
Self::FallbackAcknowledgement(inner) => inner.fmt(f),
|
Self::FallbackAcknowledgement(inner) => inner.fmt(f),
|
||||||
|
Self::Terms(inner) => inner.fmt(f),
|
||||||
Self::_Custom(inner) => inner.fmt(f),
|
Self::_Custom(inner) => inner.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,6 +225,7 @@ impl<'de> Deserialize<'de> for AuthData {
|
|||||||
Some("m.login.registration_token") => {
|
Some("m.login.registration_token") => {
|
||||||
from_raw_json_value(&json).map(Self::RegistrationToken)
|
from_raw_json_value(&json).map(Self::RegistrationToken)
|
||||||
}
|
}
|
||||||
|
Some("m.login.terms") => from_raw_json_value(&json).map(Self::Terms),
|
||||||
None => from_raw_json_value(&json).map(Self::FallbackAcknowledgement),
|
None => from_raw_json_value(&json).map(Self::FallbackAcknowledgement),
|
||||||
Some(_) => from_raw_json_value(&json).map(Self::_Custom),
|
Some(_) => from_raw_json_value(&json).map(Self::_Custom),
|
||||||
}
|
}
|
||||||
@ -253,6 +265,12 @@ pub enum AuthType {
|
|||||||
#[ruma_enum(rename = "m.login.registration_token")]
|
#[ruma_enum(rename = "m.login.registration_token")]
|
||||||
RegistrationToken,
|
RegistrationToken,
|
||||||
|
|
||||||
|
/// Terms of service (`m.login.terms`).
|
||||||
|
///
|
||||||
|
/// This type is only valid during account registration.
|
||||||
|
#[ruma_enum(rename = "m.login.terms")]
|
||||||
|
Terms,
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
_Custom(PrivOwnedStr),
|
_Custom(PrivOwnedStr),
|
||||||
}
|
}
|
||||||
@ -434,6 +452,28 @@ impl FallbackAcknowledgement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Data for terms of service flow.
|
||||||
|
///
|
||||||
|
/// This type is only valid during account registration.
|
||||||
|
///
|
||||||
|
/// See [the spec] for how to use this.
|
||||||
|
///
|
||||||
|
/// [the spec]: https://spec.matrix.org/latest/client-server-api/#terms-of-service-at-registration
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "type", rename = "m.login.terms")]
|
||||||
|
pub struct Terms {
|
||||||
|
/// The value of the session key given by the homeserver, if any.
|
||||||
|
pub session: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terms {
|
||||||
|
/// Creates an empty `Terms`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@ -551,29 +591,25 @@ pub struct IncomingCustomThirdPartyId {
|
|||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
pub struct ThirdpartyIdCredentials {
|
pub struct ThirdpartyIdCredentials {
|
||||||
/// Identity server session ID.
|
/// Identity server (or homeserver) session ID.
|
||||||
pub sid: OwnedSessionId,
|
pub sid: OwnedSessionId,
|
||||||
|
|
||||||
/// Identity server client secret.
|
/// Identity server (or homeserver) client secret.
|
||||||
pub client_secret: OwnedClientSecret,
|
pub client_secret: OwnedClientSecret,
|
||||||
|
|
||||||
/// Identity server URL.
|
/// Identity server URL.
|
||||||
pub id_server: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id_server: Option<String>,
|
||||||
|
|
||||||
/// Identity server access token.
|
/// Identity server access token.
|
||||||
pub id_access_token: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id_access_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThirdpartyIdCredentials {
|
impl ThirdpartyIdCredentials {
|
||||||
/// Creates a new `ThirdpartyIdCredentials` with the given session ID, client secret, identity
|
/// Creates a new `ThirdpartyIdCredentials` with the given session ID and client secret.
|
||||||
/// server address and access token.
|
pub fn new(sid: OwnedSessionId, client_secret: OwnedClientSecret) -> Self {
|
||||||
pub fn new(
|
Self { sid, client_secret, id_server: None, id_access_token: None }
|
||||||
sid: OwnedSessionId,
|
|
||||||
client_secret: OwnedClientSecret,
|
|
||||||
id_server: String,
|
|
||||||
id_access_token: String,
|
|
||||||
) -> Self {
|
|
||||||
Self { sid, client_secret, id_server, id_access_token }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,30 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
- The `instance_id` field was removed from `ProtocolInstanceInit` and is now an
|
||||||
|
`Option<String>` for `ProtocolInstance`. It made the `unstable-unspecified`
|
||||||
|
feature non-additive.
|
||||||
|
|
||||||
|
Breaking changes:
|
||||||
|
|
||||||
|
- Rename the `query_map` attribute of the `request` macro to `query_all`, and
|
||||||
|
remove the required bound to implement `IntoIterator<Item = (String, String)>`.
|
||||||
|
This allows to use a struct or enum as well as a map to represent the list of
|
||||||
|
query parameters. Note that the (de)serialization of the type used must work
|
||||||
|
with `serde_html_form`.
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
- Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for
|
- Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for
|
||||||
cases where we receive a HTTP header with an unexpected value.
|
cases where we receive a HTTP header with an unexpected value.
|
||||||
|
- Implement `Eq`/`Hash`/`PartialEq` for `ThirdPartyIdentifier`, to check whether
|
||||||
|
a `ThirdPartyIdentifier` has already been added by another user.
|
||||||
|
- Add `MatrixVersion::V1_11`
|
||||||
|
- Clarify in the docs of `AuthScheme` that sending an access token via a query
|
||||||
|
parameter is deprecated, according to MSC4126 / Matrix 1.11.
|
||||||
|
- Constructing a Matrix URI for an event with a room alias is deprecated,
|
||||||
|
according to MSC4132 / Matrix 1.11
|
||||||
|
|
||||||
# 0.13.0
|
# 0.13.0
|
||||||
|
|
||||||
|
@ -122,9 +122,11 @@ macro_rules! metadata {
|
|||||||
/// they are declared must match the order in which they occur in the request path.
|
/// they are declared must match the order in which they occur in the request path.
|
||||||
/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
|
/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
|
||||||
/// string.
|
/// string.
|
||||||
/// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any
|
/// * `#[ruma_api(query_all)]`: Instead of individual query fields, one query_all field, of any
|
||||||
/// type that implements `IntoIterator<Item = (String, String)>` (e.g. `HashMap<String,
|
/// type that can be (de)serialized by [serde_html_form], can be used for cases where
|
||||||
/// String>`, can be used for cases where an endpoint supports arbitrary query parameters.
|
/// multiple endpoints should share a query fields type, the query fields are better
|
||||||
|
/// expressed as an `enum` rather than a `struct`, or the endpoint supports arbitrary query
|
||||||
|
/// parameters.
|
||||||
/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
|
/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
|
||||||
/// attributes to customize (de)serialization.
|
/// attributes to customize (de)serialization.
|
||||||
/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
|
/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
|
||||||
@ -209,11 +211,13 @@ macro_rules! metadata {
|
|||||||
/// # pub struct Response {}
|
/// # pub struct Response {}
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [serde_html_form]: https://crates.io/crates/serde_html_form
|
||||||
pub use ruma_macros::request;
|
pub use ruma_macros::request;
|
||||||
/// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
|
/// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
|
||||||
///
|
///
|
||||||
/// The `OutgoingRequest` impl is feature-gated behind `cfg(feature = "client")`.
|
/// The `OutgoingResponse` impl is feature-gated behind `cfg(feature = "server")`.
|
||||||
/// The `IncomingRequest` impl is feature-gated behind `cfg(feature = "server")`.
|
/// The `IncomingResponse` impl is feature-gated behind `cfg(feature = "client")`.
|
||||||
///
|
///
|
||||||
/// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope.
|
/// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope.
|
||||||
///
|
///
|
||||||
@ -223,7 +227,7 @@ pub use ruma_macros::request;
|
|||||||
///
|
///
|
||||||
/// ## Attributes
|
/// ## Attributes
|
||||||
///
|
///
|
||||||
/// To declare which part of the request a field belongs to:
|
/// To declare which part of the response a field belongs to:
|
||||||
///
|
///
|
||||||
/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
|
/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
|
||||||
/// headers on the response. The value must implement `Display`. Generally this is a
|
/// headers on the response. The value must implement `Display`. Generally this is a
|
||||||
@ -497,19 +501,19 @@ pub enum AuthScheme {
|
|||||||
/// Authentication is performed by including an access token in the `Authentication` http
|
/// Authentication is performed by including an access token in the `Authentication` http
|
||||||
/// header, or an `access_token` query parameter.
|
/// header, or an `access_token` query parameter.
|
||||||
///
|
///
|
||||||
/// It is recommended to use the header over the query parameter.
|
/// Using the query parameter is deprecated since Matrix 1.11.
|
||||||
AccessToken,
|
AccessToken,
|
||||||
|
|
||||||
/// Authentication is optional, and it is performed by including an access token in the
|
/// Authentication is optional, and it is performed by including an access token in the
|
||||||
/// `Authentication` http header, or an `access_token` query parameter.
|
/// `Authentication` http header, or an `access_token` query parameter.
|
||||||
///
|
///
|
||||||
/// It is recommended to use the header over the query parameter.
|
/// Using the query parameter is deprecated since Matrix 1.11.
|
||||||
AccessTokenOptional,
|
AccessTokenOptional,
|
||||||
|
|
||||||
/// Authentication is only performed for appservices, by including an access token in the
|
/// Authentication is only performed for appservices, by including an access token in the
|
||||||
/// `Authentication` http header, or an `access_token` query parameter.
|
/// `Authentication` http header, or an `access_token` query parameter.
|
||||||
///
|
///
|
||||||
/// It is recommended to use the header over the query parameter.
|
/// Using the query parameter is deprecated since Matrix 1.11.
|
||||||
AppserviceToken,
|
AppserviceToken,
|
||||||
|
|
||||||
/// Authentication is performed by including X-Matrix signatures in the request headers,
|
/// Authentication is performed by including X-Matrix signatures in the request headers,
|
||||||
|
@ -541,6 +541,11 @@ pub enum MatrixVersion {
|
|||||||
///
|
///
|
||||||
/// See <https://spec.matrix.org/v1.10/>.
|
/// See <https://spec.matrix.org/v1.10/>.
|
||||||
V1_10,
|
V1_10,
|
||||||
|
|
||||||
|
/// Version 1.11 of the Matrix specification, released in Q2 2024.
|
||||||
|
///
|
||||||
|
/// See <https://spec.matrix.org/v1.11/>.
|
||||||
|
V1_11,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for MatrixVersion {
|
impl TryFrom<&str> for MatrixVersion {
|
||||||
@ -564,6 +569,7 @@ impl TryFrom<&str> for MatrixVersion {
|
|||||||
"v1.8" => V1_8,
|
"v1.8" => V1_8,
|
||||||
"v1.9" => V1_9,
|
"v1.9" => V1_9,
|
||||||
"v1.10" => V1_10,
|
"v1.10" => V1_10,
|
||||||
|
"v1.11" => V1_11,
|
||||||
_ => return Err(UnknownVersionError),
|
_ => return Err(UnknownVersionError),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -613,6 +619,7 @@ impl MatrixVersion {
|
|||||||
MatrixVersion::V1_8 => (1, 8),
|
MatrixVersion::V1_8 => (1, 8),
|
||||||
MatrixVersion::V1_9 => (1, 9),
|
MatrixVersion::V1_9 => (1, 9),
|
||||||
MatrixVersion::V1_10 => (1, 10),
|
MatrixVersion::V1_10 => (1, 10),
|
||||||
|
MatrixVersion::V1_11 => (1, 11),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,6 +637,7 @@ impl MatrixVersion {
|
|||||||
(1, 8) => Ok(MatrixVersion::V1_8),
|
(1, 8) => Ok(MatrixVersion::V1_8),
|
||||||
(1, 9) => Ok(MatrixVersion::V1_9),
|
(1, 9) => Ok(MatrixVersion::V1_9),
|
||||||
(1, 10) => Ok(MatrixVersion::V1_10),
|
(1, 10) => Ok(MatrixVersion::V1_10),
|
||||||
|
(1, 11) => Ok(MatrixVersion::V1_11),
|
||||||
_ => Err(UnknownVersionError),
|
_ => Err(UnknownVersionError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -724,7 +732,9 @@ impl MatrixVersion {
|
|||||||
// <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
|
// <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
|
||||||
| MatrixVersion::V1_9
|
| MatrixVersion::V1_9
|
||||||
// <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
|
// <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
|
||||||
| MatrixVersion::V1_10 => RoomVersionId::V10,
|
| MatrixVersion::V1_10
|
||||||
|
// <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
|
||||||
|
| MatrixVersion::V1_11 => RoomVersionId::V10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@ pub enum MatrixId {
|
|||||||
User(OwnedUserId),
|
User(OwnedUserId),
|
||||||
|
|
||||||
/// An event ID.
|
/// An event ID.
|
||||||
|
///
|
||||||
|
/// Constructing this variant from an `OwnedRoomAliasId` is deprecated, because room aliases
|
||||||
|
/// are mutable, so the URI might break after a while.
|
||||||
Event(OwnedRoomOrAliasId, OwnedEventId),
|
Event(OwnedRoomOrAliasId, OwnedEventId),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,12 +575,11 @@ mod tests {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
"https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs"
|
"https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
#[allow(deprecated)]
|
||||||
room_alias_id!("#ruma:notareal.hs")
|
let uri = room_alias_id!("#ruma:notareal.hs")
|
||||||
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
||||||
.to_string(),
|
.to_string();
|
||||||
"https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs"
|
assert_eq!(uri, "https://matrix.to/#/%23ruma:notareal.hs/$event:notareal.hs");
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
room_id!("!ruma:notareal.hs")
|
room_id!("!ruma:notareal.hs")
|
||||||
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
.matrix_to_event_uri(event_id!("$event:notareal.hs"))
|
||||||
@ -869,12 +871,11 @@ mod tests {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
"matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
|
"matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
#[allow(deprecated)]
|
||||||
room_alias_id!("#ruma:notareal.hs")
|
let uri = room_alias_id!("#ruma:notareal.hs")
|
||||||
.matrix_event_uri(event_id!("$event:notareal.hs"))
|
.matrix_event_uri(event_id!("$event:notareal.hs"))
|
||||||
.to_string(),
|
.to_string();
|
||||||
"matrix:r/ruma:notareal.hs/e/event:notareal.hs"
|
assert_eq!(uri, "matrix:r/ruma:notareal.hs/e/event:notareal.hs");
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
room_id!("!ruma:notareal.hs")
|
room_id!("!ruma:notareal.hs")
|
||||||
.matrix_event_uri(event_id!("$event:notareal.hs"))
|
.matrix_event_uri(event_id!("$event:notareal.hs"))
|
||||||
|
@ -37,6 +37,9 @@ impl RoomAliasId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a `matrix.to` URI for an event scoped under this room alias ID.
|
/// Create a `matrix.to` URI for an event scoped under this room alias ID.
|
||||||
|
///
|
||||||
|
/// This is deprecated because room aliases are mutable, so the URI might break after a while.
|
||||||
|
#[deprecated = "Use `RoomId::matrix_to_event_uri` instead."]
|
||||||
pub fn matrix_to_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixToUri {
|
pub fn matrix_to_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixToUri {
|
||||||
MatrixToUri::new((self.to_owned(), ev_id.into()).into(), Vec::new())
|
MatrixToUri::new((self.to_owned(), ev_id.into()).into(), Vec::new())
|
||||||
}
|
}
|
||||||
@ -49,6 +52,9 @@ impl RoomAliasId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a `matrix:` URI for an event scoped under this room alias ID.
|
/// Create a `matrix:` URI for an event scoped under this room alias ID.
|
||||||
|
///
|
||||||
|
/// This is deprecated because room aliases are mutable, so the URI might break after a while.
|
||||||
|
#[deprecated = "Use `RoomId::matrix_event_uri` instead."]
|
||||||
pub fn matrix_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixUri {
|
pub fn matrix_event_uri(&self, ev_id: impl Into<OwnedEventId>) -> MatrixUri {
|
||||||
MatrixUri::new((self.to_owned(), ev_id.into()).into(), Vec::new(), None)
|
MatrixUri::new((self.to_owned(), ev_id.into()).into(), Vec::new(), None)
|
||||||
}
|
}
|
||||||
|
@ -602,7 +602,7 @@ mod tests {
|
|||||||
assert!(!"m".matches_word("[[:alpha:]]?"));
|
assert!(!"m".matches_word("[[:alpha:]]?"));
|
||||||
assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?"));
|
assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?"));
|
||||||
|
|
||||||
// From the spec: <https://spec.matrix.org/v1.10/client-server-api/#conditions-1>
|
// From the spec: <https://spec.matrix.org/v1.11/client-server-api/#conditions-1>
|
||||||
assert!("An example event.".matches_word("ex*ple"));
|
assert!("An example event.".matches_word("ex*ple"));
|
||||||
assert!("exple".matches_word("ex*ple"));
|
assert!("exple".matches_word("ex*ple"));
|
||||||
assert!("An exciting triple-whammy".matches_word("ex*ple"));
|
assert!("An exciting triple-whammy".matches_word("ex*ple"));
|
||||||
@ -651,7 +651,7 @@ mod tests {
|
|||||||
assert!("".matches_pattern("*", false));
|
assert!("".matches_pattern("*", false));
|
||||||
assert!(!"foo".matches_pattern("", false));
|
assert!(!"foo".matches_pattern("", false));
|
||||||
|
|
||||||
// From the spec: <https://spec.matrix.org/v1.10/client-server-api/#conditions-1>
|
// From the spec: <https://spec.matrix.org/v1.11/client-server-api/#conditions-1>
|
||||||
assert!("Lunch plans".matches_pattern("lunc?*", false));
|
assert!("Lunch plans".matches_pattern("lunc?*", false));
|
||||||
assert!("LUNCH".matches_pattern("lunc?*", false));
|
assert!("LUNCH".matches_pattern("lunc?*", false));
|
||||||
assert!(!" lunch".matches_pattern("lunc?*", false));
|
assert!(!" lunch".matches_pattern("lunc?*", false));
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! [thirdparty]: https://spec.matrix.org/latest/client-server-api/#third-party-networks
|
//! [thirdparty]: https://spec.matrix.org/latest/client-server-api/#third-party-networks
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -91,7 +94,8 @@ pub struct ProtocolInstance {
|
|||||||
///
|
///
|
||||||
/// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
|
/// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
|
||||||
#[cfg(feature = "unstable-unspecified")]
|
#[cfg(feature = "unstable-unspecified")]
|
||||||
pub instance_id: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub instance_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initial set of fields of `Protocol`.
|
/// Initial set of fields of `Protocol`.
|
||||||
@ -109,30 +113,18 @@ pub struct ProtocolInstanceInit {
|
|||||||
|
|
||||||
/// A unique identifier across all instances.
|
/// A unique identifier across all instances.
|
||||||
pub network_id: String,
|
pub network_id: String,
|
||||||
|
|
||||||
/// A unique identifier across all instances.
|
|
||||||
///
|
|
||||||
/// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833).
|
|
||||||
#[cfg(feature = "unstable-unspecified")]
|
|
||||||
pub instance_id: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ProtocolInstanceInit> for ProtocolInstance {
|
impl From<ProtocolInstanceInit> for ProtocolInstance {
|
||||||
fn from(init: ProtocolInstanceInit) -> Self {
|
fn from(init: ProtocolInstanceInit) -> Self {
|
||||||
let ProtocolInstanceInit {
|
let ProtocolInstanceInit { desc, fields, network_id } = init;
|
||||||
desc,
|
|
||||||
fields,
|
|
||||||
network_id,
|
|
||||||
#[cfg(feature = "unstable-unspecified")]
|
|
||||||
instance_id,
|
|
||||||
} = init;
|
|
||||||
Self {
|
Self {
|
||||||
desc,
|
desc,
|
||||||
icon: None,
|
icon: None,
|
||||||
fields,
|
fields,
|
||||||
network_id,
|
network_id,
|
||||||
#[cfg(feature = "unstable-unspecified")]
|
#[cfg(feature = "unstable-unspecified")]
|
||||||
instance_id,
|
instance_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,7 +232,6 @@ pub enum Medium {
|
|||||||
/// this type using `ThirdPartyIdentifier::Init` / `.into()`.
|
/// this type using `ThirdPartyIdentifier::Init` / `.into()`.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
pub struct ThirdPartyIdentifier {
|
pub struct ThirdPartyIdentifier {
|
||||||
/// The third party identifier address.
|
/// The third party identifier address.
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@ -255,6 +246,20 @@ pub struct ThirdPartyIdentifier {
|
|||||||
pub added_at: MilliSecondsSinceUnixEpoch,
|
pub added_at: MilliSecondsSinceUnixEpoch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for ThirdPartyIdentifier {}
|
||||||
|
|
||||||
|
impl Hash for ThirdPartyIdentifier {
|
||||||
|
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||||
|
(self.medium.as_str(), &self.address).hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ThirdPartyIdentifier {
|
||||||
|
fn eq(&self, other: &ThirdPartyIdentifier) -> bool {
|
||||||
|
self.address == other.address && self.medium == other.medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Initial set of fields of `ThirdPartyIdentifier`.
|
/// Initial set of fields of `ThirdPartyIdentifier`.
|
||||||
///
|
///
|
||||||
/// This struct will not be updated even if additional fields are added to `ThirdPartyIdentifier`
|
/// This struct will not be updated even if additional fields are added to `ThirdPartyIdentifier`
|
||||||
|
@ -133,7 +133,41 @@ pub mod raw_body_endpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod query_map_endpoint {
|
pub mod query_all_enum_endpoint {
|
||||||
|
use ruma_common::{
|
||||||
|
api::{request, response, Metadata},
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum MyCustomQueryEnum {
|
||||||
|
VariantA { field_a: String },
|
||||||
|
VariantB { field_b: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
const METADATA: Metadata = metadata! {
|
||||||
|
method: GET,
|
||||||
|
rate_limited: false,
|
||||||
|
authentication: None,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/some/query/map/endpoint",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Request type for the `query_all_enum_endpoint` endpoint.
|
||||||
|
#[request]
|
||||||
|
pub struct Request {
|
||||||
|
#[ruma_api(query_all)]
|
||||||
|
pub query: MyCustomQueryEnum,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the `query_all_enum_endpoint` endpoint.
|
||||||
|
#[response]
|
||||||
|
pub struct Response {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod query_all_vec_endpoint {
|
||||||
use ruma_common::{
|
use ruma_common::{
|
||||||
api::{request, response, Metadata},
|
api::{request, response, Metadata},
|
||||||
metadata,
|
metadata,
|
||||||
@ -148,14 +182,14 @@ pub mod query_map_endpoint {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Request type for the `newtype_body_endpoint` endpoint.
|
/// Request type for the `query_all_vec_endpoint` endpoint.
|
||||||
#[request]
|
#[request]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
#[ruma_api(query_map)]
|
#[ruma_api(query_all)]
|
||||||
pub fields: Vec<(String, String)>,
|
pub fields: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response type for the `newtype_body_endpoint` endpoint.
|
/// Response type for the `query_all_vec_endpoint` endpoint.
|
||||||
#[response]
|
#[response]
|
||||||
pub struct Response {}
|
pub struct Response {}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
- Fix deserialization of `AnyGlobalAccountDataEvent` for variants with a type
|
||||||
|
fragment.
|
||||||
|
- Fix serialization of `room::message::Relation` and `room::encrypted::Relation`
|
||||||
|
which could cause duplicate `rel_type` keys.
|
||||||
|
- `Restricted` no longer fails to deserialize when the `allow` field is missing
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
- Add support for encrypted stickers as sent by several bridges under the flag `compat-encrypted-stickers`
|
- Add support for encrypted stickers as sent by several bridges under the flag `compat-encrypted-stickers`
|
||||||
|
- Add unstable support for MSC3489 `m.beacon` & `m.beacon_info` events
|
||||||
|
(unstable types `org.matrix.msc3489.beacon` & `org.matrix.msc3489.beacon_info`)
|
||||||
|
- Stabilize support for muting in VoIP calls, according to Matrix 1.11
|
||||||
|
- All the root `Any*EventContent` types now have a `EventContentFromType` implementations
|
||||||
|
automatically derived by the `event_enum!` macro.
|
||||||
|
|
||||||
Breaking changes:
|
Breaking changes:
|
||||||
|
|
||||||
- `StickerEventContent::url` was replaced by `StickerEventContent::source` which is a `StickerMediaSource`
|
- `StickerEventContent::url` was replaced by `StickerEventContent::source` which is a `StickerMediaSource`
|
||||||
|
|
||||||
# 0.28.1
|
# 0.28.1
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ all-features = true
|
|||||||
[features]
|
[features]
|
||||||
canonical-json = ["ruma-common/canonical-json"]
|
canonical-json = ["ruma-common/canonical-json"]
|
||||||
html = ["dep:ruma-html"]
|
html = ["dep:ruma-html"]
|
||||||
markdown = ["pulldown-cmark"]
|
markdown = ["dep:pulldown-cmark"]
|
||||||
unstable-exhaustive-types = []
|
unstable-exhaustive-types = []
|
||||||
unstable-msc1767 = []
|
unstable-msc1767 = []
|
||||||
unstable-msc2448 = []
|
unstable-msc2448 = []
|
||||||
@ -29,10 +29,10 @@ unstable-msc3245 = ["unstable-msc3246"]
|
|||||||
# https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md
|
# https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md
|
||||||
unstable-msc3245-v1-compat = []
|
unstable-msc3245-v1-compat = []
|
||||||
unstable-msc3246 = ["unstable-msc3927"]
|
unstable-msc3246 = ["unstable-msc3927"]
|
||||||
unstable-msc3291 = []
|
|
||||||
unstable-msc3381 = ["unstable-msc1767"]
|
unstable-msc3381 = ["unstable-msc1767"]
|
||||||
unstable-msc3401 = []
|
unstable-msc3401 = []
|
||||||
unstable-msc3488 = ["unstable-msc1767"]
|
unstable-msc3488 = ["unstable-msc1767"]
|
||||||
|
unstable-msc3489 = ["unstable-msc3488"]
|
||||||
unstable-msc3551 = ["unstable-msc3956"]
|
unstable-msc3551 = ["unstable-msc3956"]
|
||||||
unstable-msc3552 = ["unstable-msc3551"]
|
unstable-msc3552 = ["unstable-msc3551"]
|
||||||
unstable-msc3553 = ["unstable-msc3552"]
|
unstable-msc3553 = ["unstable-msc3552"]
|
||||||
@ -41,7 +41,7 @@ unstable-msc3927 = ["unstable-msc3551"]
|
|||||||
unstable-msc3954 = ["unstable-msc1767"]
|
unstable-msc3954 = ["unstable-msc1767"]
|
||||||
unstable-msc3955 = ["unstable-msc1767"]
|
unstable-msc3955 = ["unstable-msc1767"]
|
||||||
unstable-msc3956 = ["unstable-msc1767"]
|
unstable-msc3956 = ["unstable-msc1767"]
|
||||||
unstable-msc4075 = []
|
unstable-msc4075 = ["unstable-msc3401"]
|
||||||
unstable-pdu = []
|
unstable-pdu = []
|
||||||
|
|
||||||
# Allow some mandatory fields to be missing, defaulting them to an empty string
|
# Allow some mandatory fields to be missing, defaulting them to an empty string
|
||||||
@ -65,7 +65,7 @@ indexmap = { version = "2.0.0", features = ["serde"] }
|
|||||||
js_int = { workspace = true, features = ["serde"] }
|
js_int = { workspace = true, features = ["serde"] }
|
||||||
js_option = "0.1.0"
|
js_option = "0.1.0"
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
pulldown-cmark = { version = "0.10.3", optional = true, default-features = false, features = ["html"] }
|
pulldown-cmark = { version = "0.11.0", optional = true, default-features = false, features = ["html"] }
|
||||||
regex = { version = "1.5.6", default-features = false, features = ["std", "perf"] }
|
regex = { version = "1.5.6", default-features = false, features = ["std", "perf"] }
|
||||||
ruma-common = { workspace = true }
|
ruma-common = { workspace = true }
|
||||||
ruma-html = { workspace = true, optional = true }
|
ruma-html = { workspace = true, optional = true }
|
||||||
|
43
crates/ruma-events/src/beacon.rs
Normal file
43
crates/ruma-events/src/beacon.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//! Types for the `org.matrix.msc3489.beacon` event, the unstable version of
|
||||||
|
//! `m.beacon` ([MSC3489]).
|
||||||
|
//!
|
||||||
|
//! [MSC3489]: https://github.com/matrix-org/matrix-spec-proposals/pull/3489
|
||||||
|
|
||||||
|
use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedEventId};
|
||||||
|
use ruma_events::{location::LocationContent, relation::Reference};
|
||||||
|
use ruma_macros::EventContent;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The content of a beacon.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_event(type = "org.matrix.msc3672.beacon", alias = "m.beacon", kind = MessageLike)]
|
||||||
|
pub struct BeaconEventContent {
|
||||||
|
/// The beacon_info event id this relates to.
|
||||||
|
#[serde(rename = "m.relates_to")]
|
||||||
|
pub relates_to: Reference,
|
||||||
|
|
||||||
|
/// The location of the beacon.
|
||||||
|
#[serde(rename = "org.matrix.msc3488.location")]
|
||||||
|
pub location: LocationContent,
|
||||||
|
|
||||||
|
/// The timestamp of the event.
|
||||||
|
#[serde(rename = "org.matrix.msc3488.ts")]
|
||||||
|
pub ts: MilliSecondsSinceUnixEpoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeaconEventContent {
|
||||||
|
/// Creates a new `BeaconEventContent` with the given beacon_info event id, geo uri and
|
||||||
|
/// optional ts. If ts is None, the current time will be used.
|
||||||
|
pub fn new(
|
||||||
|
beacon_info_event_id: OwnedEventId,
|
||||||
|
geo_uri: String,
|
||||||
|
ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
relates_to: Reference::new(beacon_info_event_id),
|
||||||
|
location: LocationContent::new(geo_uri),
|
||||||
|
ts: ts.unwrap_or_else(MilliSecondsSinceUnixEpoch::now),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
crates/ruma-events/src/beacon_info.rs
Normal file
83
crates/ruma-events/src/beacon_info.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
//! Types for the `org.matrix.msc3489.beacon_info` state event, the unstable version of
|
||||||
|
//! `m.beacon_info` ([MSC3489]).
|
||||||
|
//!
|
||||||
|
//! [MSC3489]: https://github.com/matrix-org/matrix-spec-proposals/pull/3489
|
||||||
|
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedUserId};
|
||||||
|
use ruma_macros::EventContent;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::location::AssetContent;
|
||||||
|
|
||||||
|
/// The content of a beacon_info state.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_event(
|
||||||
|
type = "org.matrix.msc3672.beacon_info", alias = "m.beacon_info", kind = State, state_key_type = OwnedUserId
|
||||||
|
)]
|
||||||
|
pub struct BeaconInfoEventContent {
|
||||||
|
/// The description of the location.
|
||||||
|
///
|
||||||
|
/// It should be used to label the location on a map.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// Whether the user starts sharing their location.
|
||||||
|
pub live: bool,
|
||||||
|
|
||||||
|
/// The time when location sharing started.
|
||||||
|
#[serde(rename = "org.matrix.msc3488.ts")]
|
||||||
|
pub ts: MilliSecondsSinceUnixEpoch,
|
||||||
|
|
||||||
|
/// The duration that the location sharing will be live.
|
||||||
|
///
|
||||||
|
/// Meaning that the location will stop being shared at `ts + timeout`.
|
||||||
|
#[serde(default, with = "ruma_common::serde::duration::ms")]
|
||||||
|
pub timeout: Duration,
|
||||||
|
|
||||||
|
/// The asset that this message refers to.
|
||||||
|
#[serde(default, rename = "org.matrix.msc3488.asset")]
|
||||||
|
pub asset: AssetContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeaconInfoEventContent {
|
||||||
|
/// Creates a new `BeaconInfoEventContent` with the given description, live, timeout and
|
||||||
|
/// optional ts. If ts is None, the current time will be used.
|
||||||
|
pub fn new(
|
||||||
|
description: Option<String>,
|
||||||
|
timeout: Duration,
|
||||||
|
live: bool,
|
||||||
|
ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
description,
|
||||||
|
live,
|
||||||
|
ts: ts.unwrap_or_else(MilliSecondsSinceUnixEpoch::now),
|
||||||
|
timeout,
|
||||||
|
asset: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the beacon_info being live.
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
self.live = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the beacon_info from being live.
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.live = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start time plus its timeout, it returns `false`, indicating that the beacon is not live.
|
||||||
|
/// Otherwise, it returns `true`.
|
||||||
|
pub fn is_live(&self) -> bool {
|
||||||
|
self.live
|
||||||
|
&& self
|
||||||
|
.ts
|
||||||
|
.to_system_time()
|
||||||
|
.and_then(|t| t.checked_add(self.timeout))
|
||||||
|
.is_some_and(|t| t > SystemTime::now())
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@ pub mod negotiate;
|
|||||||
#[cfg(feature = "unstable-msc4075")]
|
#[cfg(feature = "unstable-msc4075")]
|
||||||
pub mod notify;
|
pub mod notify;
|
||||||
pub mod reject;
|
pub mod reject;
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
|
||||||
pub mod sdp_stream_metadata_changed;
|
pub mod sdp_stream_metadata_changed;
|
||||||
pub mod select_answer;
|
pub mod select_answer;
|
||||||
|
|
||||||
@ -59,14 +58,12 @@ pub struct StreamMetadata {
|
|||||||
/// Whether the audio track of the stream is muted.
|
/// Whether the audio track of the stream is muted.
|
||||||
///
|
///
|
||||||
/// Defaults to `false`.
|
/// Defaults to `false`.
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||||
pub audio_muted: bool,
|
pub audio_muted: bool,
|
||||||
|
|
||||||
/// Whether the video track of the stream is muted.
|
/// Whether the video track of the stream is muted.
|
||||||
///
|
///
|
||||||
/// Defaults to `false`.
|
/// Defaults to `false`.
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||||
pub video_muted: bool,
|
pub video_muted: bool,
|
||||||
}
|
}
|
||||||
@ -74,13 +71,7 @@ pub struct StreamMetadata {
|
|||||||
impl StreamMetadata {
|
impl StreamMetadata {
|
||||||
/// Creates a new `StreamMetadata` with the given purpose.
|
/// Creates a new `StreamMetadata` with the given purpose.
|
||||||
pub fn new(purpose: StreamPurpose) -> Self {
|
pub fn new(purpose: StreamPurpose) -> Self {
|
||||||
Self {
|
Self { purpose, audio_muted: false, video_muted: false }
|
||||||
purpose,
|
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
|
||||||
audio_muted: false,
|
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
|
||||||
video_muted: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,65 +1,118 @@
|
|||||||
//! Types for matrixRTC state events ([MSC3401]).
|
//! Types for MatrixRTC state events ([MSC3401]).
|
||||||
//!
|
//!
|
||||||
//! This implements a newer/updated version of MSC3401.
|
//! This implements a newer/updated version of MSC3401.
|
||||||
//!
|
//!
|
||||||
//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
|
//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
|
||||||
|
|
||||||
use std::time::Duration;
|
mod focus;
|
||||||
|
mod member_data;
|
||||||
|
|
||||||
use as_variant::as_variant;
|
pub use focus::*;
|
||||||
use ruma_common::{serde::StringEnum, MilliSecondsSinceUnixEpoch, OwnedUserId};
|
pub use member_data::*;
|
||||||
use ruma_macros::EventContent;
|
use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedUserId};
|
||||||
|
use ruma_macros::{EventContent, StringEnum};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::PrivOwnedStr;
|
use crate::{
|
||||||
|
PossiblyRedactedStateEventContent, PrivOwnedStr, RedactContent, RedactedStateEventContent,
|
||||||
|
StateEventType,
|
||||||
|
};
|
||||||
|
|
||||||
/// The member state event for a matrixRTC session.
|
/// The member state event for a MatrixRTC session.
|
||||||
///
|
///
|
||||||
/// This is the object containing all the data related to a matrix users participation in a
|
/// This is the object containing all the data related to a Matrix users participation in a
|
||||||
/// matrixRTC session. It consists of memberships / sessions.
|
/// MatrixRTC session.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
///
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// This is a unit struct with the enum [`CallMemberEventContent`] because a Ruma state event cannot
|
||||||
|
/// be an enum and we need this to be an untagged enum for parsing purposes. (see
|
||||||
|
/// [`CallMemberEventContent`])
|
||||||
|
///
|
||||||
|
/// This struct also exposes allows to call the methods from [`CallMemberEventContent`].
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)]
|
||||||
|
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = OwnedUserId, custom_redacted, custom_possibly_redacted)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = OwnedUserId)]
|
#[serde(untagged)]
|
||||||
pub struct CallMemberEventContent {
|
pub enum CallMemberEventContent {
|
||||||
/// A list of all the memberships that user currently has in this room.
|
/// The legacy format for m.call.member events. (An array of memberships. The devices of one
|
||||||
///
|
/// user.)
|
||||||
/// There can be multiple ones in cases the user participates with multiple devices or there
|
LegacyContent(LegacyMembershipContent),
|
||||||
/// are multiple RTC applications running.
|
/// Normal membership events. One event per membership. Multiple state keys will
|
||||||
///
|
/// be used to describe multiple devices for one user.
|
||||||
/// e.g. a call and a spacial experience.
|
SessionContent(SessionMembershipData),
|
||||||
///
|
/// An empty content means this user has been in a rtc session but is not anymore.
|
||||||
/// Important: This includes expired memberships.
|
Empty(EmptyMembershipData),
|
||||||
/// To retrieve a list including only valid memberships,
|
|
||||||
/// see [`active_memberships`](CallMemberEventContent::active_memberships).
|
|
||||||
pub memberships: Vec<Membership>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallMemberEventContent {
|
impl CallMemberEventContent {
|
||||||
/// Creates a new `CallMemberEventContent`.
|
/// Creates a new [`CallMemberEventContent`] with [`LegacyMembershipData`].
|
||||||
pub fn new(memberships: Vec<Membership>) -> Self {
|
pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
|
||||||
Self { memberships }
|
Self::LegacyContent(LegacyMembershipContent {
|
||||||
|
memberships, //: memberships.into_iter().map(MembershipData::Legacy).collect(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`CallMemberEventContent`] with [`SessionMembershipData`].
|
||||||
|
pub fn new(
|
||||||
|
application: Application,
|
||||||
|
device_id: String,
|
||||||
|
focus_active: ActiveFocus,
|
||||||
|
foci_preferred: Vec<Focus>,
|
||||||
|
created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> Self {
|
||||||
|
Self::SessionContent(SessionMembershipData {
|
||||||
|
application,
|
||||||
|
device_id,
|
||||||
|
focus_active,
|
||||||
|
foci_preferred,
|
||||||
|
created_ts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Empty [`CallMemberEventContent`] representing a left membership.
|
||||||
|
pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self {
|
||||||
|
Self::Empty(EmptyMembershipData { leave_reason })
|
||||||
|
}
|
||||||
/// All non expired memberships in this member event.
|
/// All non expired memberships in this member event.
|
||||||
///
|
///
|
||||||
/// In most cases you want tu use this method instead of the public memberships field.
|
/// In most cases you want to use this method instead of the public memberships field.
|
||||||
/// The memberships field will also include expired events.
|
/// The memberships field will also include expired events.
|
||||||
///
|
///
|
||||||
|
/// This copies all the memberships and converts them
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in case
|
/// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in the
|
||||||
/// the Membership does not contain `created_ts`. (`origin_server_ts` will be ignored if
|
/// Membership does not contain [`LegacyMembershipData::created_ts`]. (`origin_server_ts` will
|
||||||
/// `created_ts` is `Some`)
|
/// be ignored if [`LegacyMembershipData::created_ts`] is `Some`)
|
||||||
pub fn active_memberships(
|
pub fn active_memberships(
|
||||||
&self,
|
&self,
|
||||||
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
) -> Vec<&Membership> {
|
) -> Vec<MembershipData<'_>> {
|
||||||
self.memberships.iter().filter(|m| !m.is_expired(origin_server_ts)).collect()
|
match self {
|
||||||
|
CallMemberEventContent::LegacyContent(content) => {
|
||||||
|
content.active_memberships(origin_server_ts)
|
||||||
|
}
|
||||||
|
CallMemberEventContent::SessionContent(content) => {
|
||||||
|
[content].map(MembershipData::Session).to_vec()
|
||||||
|
}
|
||||||
|
CallMemberEventContent::Empty(_) => Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the `created_ts` of each [Membership] in this event.
|
/// All the memberships for this event. Can only contain multiple elements in the case of legacy
|
||||||
|
/// `m.call.member` state events.
|
||||||
|
pub fn memberships(&self) -> Vec<MembershipData<'_>> {
|
||||||
|
match self {
|
||||||
|
CallMemberEventContent::LegacyContent(content) => {
|
||||||
|
content.memberships.iter().map(MembershipData::Legacy).collect()
|
||||||
|
}
|
||||||
|
CallMemberEventContent::SessionContent(content) => {
|
||||||
|
[content].map(MembershipData::Session).to_vec()
|
||||||
|
}
|
||||||
|
CallMemberEventContent::Empty(_) => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `created_ts` of each [`MembershipData::Legacy`] in this event.
|
||||||
///
|
///
|
||||||
/// Each call member event contains the `origin_server_ts` and `content.create_ts`.
|
/// Each call member event contains the `origin_server_ts` and `content.create_ts`.
|
||||||
/// `content.create_ts` is undefined for the initial event of a session (because the
|
/// `content.create_ts` is undefined for the initial event of a session (because the
|
||||||
@ -68,257 +121,130 @@ impl CallMemberEventContent {
|
|||||||
/// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
|
/// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
|
||||||
/// expired)
|
/// expired)
|
||||||
pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
|
pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
|
||||||
self.memberships.iter_mut().for_each(|m| {
|
match self {
|
||||||
|
CallMemberEventContent::LegacyContent(content) => {
|
||||||
|
content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| {
|
||||||
m.created_ts.get_or_insert(origin_server_ts);
|
m.created_ts.get_or_insert(origin_server_ts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
CallMemberEventContent::SessionContent(m) => {
|
||||||
|
m.created_ts.get_or_insert(origin_server_ts);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A membership describes one of the sessions this user currently partakes.
|
/// This describes the CallMember event if the user is not part of the current session.
|
||||||
///
|
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
||||||
/// 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)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
pub struct Membership {
|
pub struct EmptyMembershipData {
|
||||||
/// The type of the matrixRTC session the membership belongs to.
|
/// 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)
|
||||||
/// 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
pub leave_reason: Option<LeaveReason>,
|
||||||
|
|
||||||
/// 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 {
|
/// This is the optional value for an empty membership event content:
|
||||||
/// The application of the membership is "m.call" and the scope is "m.room".
|
/// [`CallMemberEventContent::Empty`]. It is used when the user disconnected and a Future ([MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140))
|
||||||
pub fn is_room_call(&self) -> bool {
|
/// was used to update the membership after the client was not reachable anymore.
|
||||||
as_variant!(&self.application, Application::Call)
|
|
||||||
.is_some_and(|call| call.scope == CallScope::Room)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The application of the membership is "m.call".
|
|
||||||
pub fn is_call(&self) -> bool {
|
|
||||||
as_variant!(&self.application, Application::Call).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the event is expired.
|
|
||||||
///
|
|
||||||
/// Defaults to using `created_ts` of the `Membership`.
|
|
||||||
/// If no `origin_server_ts` is provided and the event does not contain `created_ts`
|
|
||||||
/// the event will be considered as not expired.
|
|
||||||
/// In this case, a warning will be logged.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `origin_server_ts` - a fallback if `created_ts` is not present
|
|
||||||
pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
|
|
||||||
let ev_created_ts = self.created_ts.or(origin_server_ts);
|
|
||||||
|
|
||||||
if let Some(ev_created_ts) = ev_created_ts {
|
|
||||||
let now = MilliSecondsSinceUnixEpoch::now().to_system_time();
|
|
||||||
let expire_ts = ev_created_ts.to_system_time().map(|t| t + self.expires);
|
|
||||||
now > expire_ts
|
|
||||||
} else {
|
|
||||||
// This should not be reached since we only allow events that have copied over
|
|
||||||
// the origin server ts. `set_created_ts_if_none`
|
|
||||||
warn!("Encountered a Call Member state event where the origin_ts (or origin_server_ts) could not be found.\
|
|
||||||
It is treated as a non expired event but this might be wrong.");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initial set of fields of [`Membership`].
|
|
||||||
#[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))]
|
|
||||||
#[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 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"))]
|
|
||||||
#[derive(Clone, PartialEq, StringEnum)]
|
#[derive(Clone, PartialEq, StringEnum)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_enum(rename_all = "m.snake_case")]
|
#[ruma_enum(rename_all = "m.snake_case")]
|
||||||
pub enum CallScope {
|
pub enum LeaveReason {
|
||||||
/// A call which every user of a room can join and create.
|
/// The user left the call by losing network connection or closing
|
||||||
///
|
/// the client before it was able to send the leave event.
|
||||||
/// There is no particular name associated with it.
|
LostConnection,
|
||||||
///
|
|
||||||
/// There can only be one per room.
|
|
||||||
Room,
|
|
||||||
|
|
||||||
/// A user call is owned by a user.
|
|
||||||
///
|
|
||||||
/// Each user can create one there can be multiple per room. They are started and ended by the
|
|
||||||
/// owning user.
|
|
||||||
User,
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
_Custom(PrivOwnedStr),
|
_Custom(PrivOwnedStr),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RedactContent for CallMemberEventContent {
|
||||||
|
type Redacted = RedactedCallMemberEventContent;
|
||||||
|
|
||||||
|
fn redact(self, _version: &ruma_common::RoomVersionId) -> Self::Redacted {
|
||||||
|
RedactedCallMemberEventContent {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PossiblyRedacted version of [`CallMemberEventContent`].
|
||||||
|
///
|
||||||
|
/// Since [`CallMemberEventContent`] has the [`CallMemberEventContent::Empty`] state it already is
|
||||||
|
/// compatible with the redacted version of the state event content.
|
||||||
|
pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
|
||||||
|
|
||||||
|
impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
|
||||||
|
type StateKey = OwnedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Redacted version of [`CallMemberEventContent`].
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[allow(clippy::exhaustive_structs)]
|
||||||
|
pub struct RedactedCallMemberEventContent {}
|
||||||
|
|
||||||
|
impl ruma_events::content::EventContent for RedactedCallMemberEventContent {
|
||||||
|
type EventType = StateEventType;
|
||||||
|
fn event_type(&self) -> Self::EventType {
|
||||||
|
StateEventType::CallMember
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedactedStateEventContent for RedactedCallMemberEventContent {
|
||||||
|
type StateKey = OwnedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct LegacyMembershipContent {
|
||||||
|
/// A list of all the memberships that user currently has in this room.
|
||||||
|
///
|
||||||
|
/// There can be multiple ones in case the user participates with multiple devices or there
|
||||||
|
/// are multiple RTC applications running.
|
||||||
|
///
|
||||||
|
/// e.g. a call and a spacial experience.
|
||||||
|
///
|
||||||
|
/// Important: This includes expired memberships.
|
||||||
|
/// To retrieve a list including only valid memberships,
|
||||||
|
/// see [`active_memberships`](CallMemberEventContent::active_memberships).
|
||||||
|
memberships: Vec<LegacyMembershipData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegacyMembershipContent {
|
||||||
|
fn active_memberships(
|
||||||
|
&self,
|
||||||
|
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> Vec<MembershipData<'_>> {
|
||||||
|
self.memberships
|
||||||
|
.iter()
|
||||||
|
.filter(|m| !m.is_expired(origin_server_ts))
|
||||||
|
.map(MembershipData::Legacy)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use ruma_common::MilliSecondsSinceUnixEpoch as TS;
|
use assert_matches2::assert_matches;
|
||||||
use serde_json::json;
|
use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId};
|
||||||
|
use serde_json::{from_value as from_json_value, json};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Application, CallApplicationContent, CallMemberEventContent, CallScope, Focus,
|
focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
|
||||||
LivekitFocus, Membership,
|
member_data::{
|
||||||
|
Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
|
||||||
|
},
|
||||||
|
CallMemberEventContent,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
|
||||||
|
AnyStateEvent, StateEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn create_call_member_event_content() -> CallMemberEventContent {
|
fn create_call_member_legacy_event_content() -> CallMemberEventContent {
|
||||||
CallMemberEventContent::new(vec![Membership {
|
CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
|
||||||
application: Application::Call(CallApplicationContent {
|
application: Application::Call(CallApplicationContent {
|
||||||
call_id: "123456".to_owned(),
|
call_id: "123456".to_owned(),
|
||||||
scope: CallScope::Room,
|
scope: CallScope::Room,
|
||||||
@ -334,8 +260,60 @@ mod tests {
|
|||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_call_member_event_content() -> CallMemberEventContent {
|
||||||
|
CallMemberEventContent::new(
|
||||||
|
Application::Call(CallApplicationContent {
|
||||||
|
call_id: "123456".to_owned(),
|
||||||
|
scope: CallScope::Room,
|
||||||
|
}),
|
||||||
|
"ABCDE".to_owned(),
|
||||||
|
ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: FocusSelection::OldestMembership,
|
||||||
|
}),
|
||||||
|
vec![Focus::Livekit(LivekitFocus {
|
||||||
|
alias: "1".to_owned(),
|
||||||
|
service_url: "https://livekit.com".to_owned(),
|
||||||
|
})],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_call_member_event_content() {
|
fn serialize_call_member_event_content() {
|
||||||
|
let call_member_event = &json!({
|
||||||
|
"application": "m.call",
|
||||||
|
"call_id": "123456",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": "ABCDE",
|
||||||
|
"foci_preferred": [
|
||||||
|
{
|
||||||
|
"livekit_alias": "1",
|
||||||
|
"livekit_service_url": "https://livekit.com",
|
||||||
|
"type": "livekit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"focus_active":{
|
||||||
|
"type":"livekit",
|
||||||
|
"focus_select":"oldest_membership"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
call_member_event,
|
||||||
|
&serde_json::to_value(create_call_member_event_content()).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty_call_member_event = &json!({});
|
||||||
|
assert_eq!(
|
||||||
|
empty_call_member_event,
|
||||||
|
&serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
|
||||||
|
leave_reason: None
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_legacy_call_member_event_content() {
|
||||||
let call_member_event = &json!({
|
let call_member_event = &json!({
|
||||||
"memberships": [
|
"memberships": [
|
||||||
{
|
{
|
||||||
@ -358,14 +336,62 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
call_member_event,
|
call_member_event,
|
||||||
&serde_json::to_value(create_call_member_event_content()).unwrap()
|
&serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn deserialize_call_member_event_content() {
|
||||||
|
let call_member_ev = CallMemberEventContent::new(
|
||||||
|
Application::Call(CallApplicationContent {
|
||||||
|
call_id: "123456".to_owned(),
|
||||||
|
scope: CallScope::Room,
|
||||||
|
}),
|
||||||
|
"THIS_DEVICE".to_owned(),
|
||||||
|
ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: FocusSelection::OldestMembership,
|
||||||
|
}),
|
||||||
|
vec![Focus::Livekit(LivekitFocus {
|
||||||
|
alias: "room1".to_owned(),
|
||||||
|
service_url: "https://livekit1.com".to_owned(),
|
||||||
|
})],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let call_member_ev_json = json!({
|
||||||
|
"application": "m.call",
|
||||||
|
"call_id": "123456",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": "THIS_DEVICE",
|
||||||
|
"focus_active":{
|
||||||
|
"type": "livekit",
|
||||||
|
"focus_select": "oldest_membership"
|
||||||
|
},
|
||||||
|
"foci_preferred": [
|
||||||
|
{
|
||||||
|
"livekit_alias": "room1",
|
||||||
|
"livekit_service_url": "https://livekit1.com",
|
||||||
|
"type": "livekit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let ev_content: CallMemberEventContent =
|
||||||
|
serde_json::from_value(call_member_ev_json).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&ev_content).unwrap(),
|
||||||
|
serde_json::to_string(&call_member_ev).unwrap()
|
||||||
|
);
|
||||||
|
let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&json!({})).unwrap(),
|
||||||
|
serde_json::to_string(&empty).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_call_member_event_content() {
|
fn deserialize_legacy_call_member_event_content() {
|
||||||
let call_member_ev: CallMemberEventContent = CallMemberEventContent::new(vec![
|
let call_member_ev = CallMemberEventContent::new_legacy(vec![
|
||||||
Membership {
|
LegacyMembershipData {
|
||||||
application: Application::Call(CallApplicationContent {
|
application: Application::Call(CallApplicationContent {
|
||||||
call_id: "123456".to_owned(),
|
call_id: "123456".to_owned(),
|
||||||
scope: CallScope::Room,
|
scope: CallScope::Room,
|
||||||
@ -379,7 +405,7 @@ mod tests {
|
|||||||
membership_id: "0".to_owned(),
|
membership_id: "0".to_owned(),
|
||||||
created_ts: None,
|
created_ts: None,
|
||||||
},
|
},
|
||||||
Membership {
|
LegacyMembershipData {
|
||||||
application: Application::Call(CallApplicationContent {
|
application: Application::Call(CallApplicationContent {
|
||||||
call_id: "".to_owned(),
|
call_id: "".to_owned(),
|
||||||
scope: CallScope::Room,
|
scope: CallScope::Room,
|
||||||
@ -432,7 +458,85 @@ mod tests {
|
|||||||
|
|
||||||
let ev_content: CallMemberEventContent =
|
let ev_content: CallMemberEventContent =
|
||||||
serde_json::from_value(call_member_ev_json).unwrap();
|
serde_json::from_value(call_member_ev_json).unwrap();
|
||||||
assert_eq!(ev_content, call_member_ev);
|
assert_eq!(
|
||||||
|
serde_json::to_string(&ev_content).unwrap(),
|
||||||
|
serde_json::to_string(&call_member_ev).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn deserialize_member_event() {
|
||||||
|
let ev = json!({
|
||||||
|
"content":{
|
||||||
|
"application": "m.call",
|
||||||
|
"call_id": "",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": "THIS_DEVICE",
|
||||||
|
"focus_active":{
|
||||||
|
"type": "livekit",
|
||||||
|
"focus_select": "oldest_membership"
|
||||||
|
},
|
||||||
|
"foci_preferred": [
|
||||||
|
{
|
||||||
|
"livekit_alias": "room1",
|
||||||
|
"livekit_service_url": "https://livekit1.com",
|
||||||
|
"type": "livekit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"type": "m.call.member",
|
||||||
|
"origin_server_ts": 111,
|
||||||
|
"event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
|
||||||
|
"room_id": "!1234:example.org",
|
||||||
|
"sender": "@user:example.org",
|
||||||
|
"state_key":"@user:example.org",
|
||||||
|
"unsigned":{
|
||||||
|
"age":10,
|
||||||
|
"prev_content": {},
|
||||||
|
"prev_sender":"@user:example.org",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
from_json_value(ev),
|
||||||
|
Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event)))
|
||||||
|
);
|
||||||
|
|
||||||
|
let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
|
||||||
|
let sender = OwnedUserId::try_from("@user:example.org").unwrap();
|
||||||
|
let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
|
||||||
|
assert_eq!(member_event.state_key, sender);
|
||||||
|
assert_eq!(member_event.event_id, event_id);
|
||||||
|
assert_eq!(member_event.sender, sender);
|
||||||
|
assert_eq!(member_event.room_id, room_id);
|
||||||
|
assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap()));
|
||||||
|
assert_eq!(
|
||||||
|
member_event.content,
|
||||||
|
CallMemberEventContent::SessionContent(SessionMembershipData {
|
||||||
|
application: Application::Call(CallApplicationContent {
|
||||||
|
call_id: "".to_owned(),
|
||||||
|
scope: CallScope::Room
|
||||||
|
}),
|
||||||
|
device_id: "THIS_DEVICE".to_owned(),
|
||||||
|
foci_preferred: [Focus::Livekit(LivekitFocus {
|
||||||
|
alias: "room1".to_owned(),
|
||||||
|
service_url: "https://livekit1.com".to_owned()
|
||||||
|
})]
|
||||||
|
.to_vec(),
|
||||||
|
focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: FocusSelection::OldestMembership
|
||||||
|
}),
|
||||||
|
created_ts: None
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(js_int::Int::new(10), member_event.unsigned.age);
|
||||||
|
assert_eq!(
|
||||||
|
CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
|
||||||
|
member_event.unsigned.prev_content.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert_eq!(, StateUnsigned { age: 10, transaction_id: None, prev_content:
|
||||||
|
// CallMemberEventContent::Empty { leave_reason: None }, relations: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timestamps() -> (TS, TS, TS) {
|
fn timestamps() -> (TS, TS, TS) {
|
||||||
@ -449,44 +553,59 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn membership_do_expire() {
|
fn memberships_do_expire() {
|
||||||
let content = create_call_member_event_content();
|
let content_legacy = create_call_member_legacy_event_content();
|
||||||
let (now, one_second_ago, two_hours_ago) = timestamps();
|
let (now, one_second_ago, two_hours_ago) = timestamps();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content.active_memberships(Some(one_second_ago)),
|
content_legacy.active_memberships(Some(one_second_ago)),
|
||||||
content.memberships.iter().collect::<Vec<&Membership>>()
|
content_legacy.memberships()
|
||||||
);
|
);
|
||||||
|
assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content.active_memberships(Some(now)),
|
content_legacy.active_memberships(Some(two_hours_ago)),
|
||||||
content.memberships.iter().collect::<Vec<&Membership>>()
|
(vec![] as Vec<MembershipData<'_>>)
|
||||||
|
);
|
||||||
|
// session do never expire
|
||||||
|
let content_session = create_call_member_event_content();
|
||||||
|
assert_eq!(
|
||||||
|
content_session.active_memberships(Some(one_second_ago)),
|
||||||
|
content_session.memberships()
|
||||||
|
);
|
||||||
|
assert_eq!(content_session.active_memberships(Some(now)), content_session.memberships());
|
||||||
|
assert_eq!(
|
||||||
|
content_session.active_memberships(Some(two_hours_ago)),
|
||||||
|
content_session.memberships()
|
||||||
);
|
);
|
||||||
assert_eq!(content.active_memberships(Some(two_hours_ago)), vec![] as Vec<&Membership>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_created_ts() {
|
fn set_created_ts() {
|
||||||
let mut content_now = create_call_member_event_content();
|
let mut content_now = create_call_member_legacy_event_content();
|
||||||
let mut content_two_hours_ago = create_call_member_event_content();
|
let mut content_two_hours_ago = create_call_member_legacy_event_content();
|
||||||
let mut content_one_second_ago = create_call_member_event_content();
|
let mut content_one_second_ago = create_call_member_legacy_event_content();
|
||||||
let (now, one_second_ago, two_hours_ago) = timestamps();
|
let (now, one_second_ago, two_hours_ago) = timestamps();
|
||||||
|
|
||||||
content_now.set_created_ts_if_none(now);
|
content_now.set_created_ts_if_none(now);
|
||||||
content_one_second_ago.set_created_ts_if_none(one_second_ago);
|
content_one_second_ago.set_created_ts_if_none(one_second_ago);
|
||||||
content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
|
content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
|
||||||
assert_eq!(
|
assert_eq!(content_now.active_memberships(None), content_now.memberships());
|
||||||
content_now.active_memberships(None),
|
|
||||||
content_now.memberships.iter().collect::<Vec<&Membership>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(content_two_hours_ago.active_memberships(None), vec![] as Vec<&Membership>);
|
assert_eq!(
|
||||||
|
content_two_hours_ago.active_memberships(None),
|
||||||
|
vec![] as Vec<MembershipData<'_>>
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_one_second_ago.active_memberships(None),
|
content_one_second_ago.active_memberships(None),
|
||||||
content_one_second_ago.memberships.iter().collect::<Vec<&Membership>>()
|
content_one_second_ago.memberships()
|
||||||
);
|
);
|
||||||
|
|
||||||
// created_ts should not be overwritten.
|
// created_ts should not be overwritten.
|
||||||
content_two_hours_ago.set_created_ts_if_none(one_second_ago);
|
content_two_hours_ago.set_created_ts_if_none(one_second_ago);
|
||||||
// There still should be no active membership.
|
// There still should be no active membership.
|
||||||
assert_eq!(content_two_hours_ago.active_memberships(None), vec![] as Vec<&Membership>);
|
assert_eq!(
|
||||||
|
content_two_hours_ago.active_memberships(None),
|
||||||
|
vec![] as Vec<MembershipData<'_>>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
crates/ruma-events/src/call/member/focus.rs
Normal file
88
crates/ruma-events/src/call/member/focus.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
//! Types for MatrixRTC Focus/SFU configurations.
|
||||||
|
|
||||||
|
use ruma_macros::StringEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
|
/// Description of the SFU/Focus a membership can be connected to.
|
||||||
|
///
|
||||||
|
/// A focus can be any server powering the MatrixRTC session (SFU,
|
||||||
|
/// MCU). It serves as a node to redistribute RTC streams.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum Focus {
|
||||||
|
/// LiveKit is one possible type of SFU/Focus that can be used for a MatrixRTC session.
|
||||||
|
Livekit(LivekitFocus),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The struct to describe LiveKit as a `preferred_foci`.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct LivekitFocus {
|
||||||
|
/// The alias where the LiveKit sessions can be reached.
|
||||||
|
#[serde(rename = "livekit_alias")]
|
||||||
|
pub alias: String,
|
||||||
|
|
||||||
|
/// The URL of the JWT service for the LiveKit instance.
|
||||||
|
#[serde(rename = "livekit_service_url")]
|
||||||
|
pub service_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LivekitFocus {
|
||||||
|
/// Initialize a [`LivekitFocus`].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `alias` - The alias with which the LiveKit sessions can be reached.
|
||||||
|
/// * `service_url` - The url of the JWT server for the LiveKit instance.
|
||||||
|
pub fn new(alias: String, service_url: String) -> Self {
|
||||||
|
Self { alias, service_url }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data to define the actively used Focus.
|
||||||
|
///
|
||||||
|
/// A focus can be any server powering the MatrixRTC session (SFU,
|
||||||
|
/// MCU). It serves as a node to redistribute RTC streams.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum ActiveFocus {
|
||||||
|
/// LiveKit is one possible type of SFU/Focus that can be used for a MatrixRTC session.
|
||||||
|
Livekit(ActiveLivekitFocus),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fields to describe the `active_foci`.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct ActiveLivekitFocus {
|
||||||
|
/// The selection method used to select the LiveKit focus for the rtc session.
|
||||||
|
pub focus_select: FocusSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveLivekitFocus {
|
||||||
|
/// Initialize a [`ActiveLivekitFocus`].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `focus_select` - The selection method used to select the LiveKit focus for the rtc
|
||||||
|
/// session.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { focus_select: FocusSelection::OldestMembership }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to select the active focus for LiveKit
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||||
|
#[derive(Clone, PartialEq, StringEnum)]
|
||||||
|
#[ruma_enum(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub enum FocusSelection {
|
||||||
|
/// Select the active focus by using the oldest membership and the oldest focus.
|
||||||
|
OldestMembership,
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
_Custom(PrivOwnedStr),
|
||||||
|
}
|
319
crates/ruma-events/src/call/member/member_data.rs
Normal file
319
crates/ruma-events/src/call/member/member_data.rs
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
//! Types for MatrixRTC `m.call.member` state event content data ([MSC3401])
|
||||||
|
//!
|
||||||
|
//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use as_variant::as_variant;
|
||||||
|
use ruma_common::MilliSecondsSinceUnixEpoch;
|
||||||
|
use ruma_macros::StringEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use super::focus::{ActiveFocus, ActiveLivekitFocus, Focus};
|
||||||
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
|
/// The data object that contains the information for one membership.
|
||||||
|
///
|
||||||
|
/// It can be a legacy or a normal MatrixRTC Session membership.
|
||||||
|
///
|
||||||
|
/// The legacy format contains time information to compute if it is expired or not.
|
||||||
|
/// SessionMembershipData does not have the concept of timestamp based expiration anymore.
|
||||||
|
/// The state event will reliably be set to empty when the user disconnects.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub enum MembershipData<'a> {
|
||||||
|
/// The legacy format (using an array of memberships for each device -> one event per user)
|
||||||
|
Legacy(&'a LegacyMembershipData),
|
||||||
|
/// One event per device. `SessionMembershipData` contains all the information required to
|
||||||
|
/// represent the current membership state of one device.
|
||||||
|
Session(&'a SessionMembershipData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MembershipData<'a> {
|
||||||
|
/// The application this RTC membership participates in (the session type, can be `m.call`...)
|
||||||
|
pub fn application(&self) -> &Application {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => &data.application,
|
||||||
|
MembershipData::Session(data) => &data.application,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
pub fn device_id(&self) -> &String {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => &data.device_id,
|
||||||
|
MembershipData::Session(data) => &data.device_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active focus is a FocusType specific object that describes how this user
|
||||||
|
/// is currently connected.
|
||||||
|
///
|
||||||
|
/// It can use the foci_preferred list to choose one of the available (preferred)
|
||||||
|
/// foci or specific information on how to connect to this user.
|
||||||
|
///
|
||||||
|
/// Every user needs to converge to use the same focus_active type.
|
||||||
|
pub fn focus_active(&self) -> &ActiveFocus {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(_) => &ActiveFocus::Livekit(ActiveLivekitFocus {
|
||||||
|
focus_select: super::focus::FocusSelection::OldestMembership,
|
||||||
|
}),
|
||||||
|
MembershipData::Session(data) => &data.focus_active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of available/preferred options this user provides to connect to the call.
|
||||||
|
pub fn foci_preferred(&self) -> &Vec<Focus> {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => &data.foci_active,
|
||||||
|
MembershipData::Session(data) => &data.foci_preferred,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application of the membership is "m.call" and the scope is "m.room".
|
||||||
|
pub fn is_room_call(&self) -> bool {
|
||||||
|
as_variant!(self.application(), Application::Call)
|
||||||
|
.is_some_and(|call| call.scope == CallScope::Room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application of the membership is "m.call".
|
||||||
|
pub fn is_call(&self) -> bool {
|
||||||
|
as_variant!(self.application(), Application::Call).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the event is expired. This is only relevant for LegacyMembershipData
|
||||||
|
/// returns `false` if its SessionMembershipData
|
||||||
|
pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => data.is_expired(origin_server_ts),
|
||||||
|
MembershipData::Session(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the created_ts of the event.
|
||||||
|
///
|
||||||
|
/// This is the `origin_server_ts` for session data.
|
||||||
|
/// For legacy events this can either be the origin server ts or a copy from the
|
||||||
|
/// `origin_server_ts` since we expect legacy events to get updated (when a new device
|
||||||
|
/// joins/leaves).
|
||||||
|
pub fn created_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||||
|
match self {
|
||||||
|
MembershipData::Legacy(data) => data.created_ts,
|
||||||
|
MembershipData::Session(data) => data.created_ts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A membership describes one of the sessions this user currently partakes.
|
||||||
|
///
|
||||||
|
/// The application defines the type of the session.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct LegacyMembershipData {
|
||||||
|
/// The type of the MatrixRTC session the membership belongs to.
|
||||||
|
///
|
||||||
|
/// e.g. call, spacial, document...
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub application: Application,
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
///
|
||||||
|
/// The same user can join with their phone/computer.
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// The duration in milliseconds relative to the time this membership joined
|
||||||
|
/// during which the membership is valid.
|
||||||
|
///
|
||||||
|
/// The time a member has joined is defined as:
|
||||||
|
/// `MIN(content.created_ts, event.origin_server_ts)`
|
||||||
|
#[serde(with = "ruma_common::serde::duration::ms")]
|
||||||
|
pub expires: Duration,
|
||||||
|
|
||||||
|
/// Stores a copy of the `origin_server_ts` of the initial session event.
|
||||||
|
///
|
||||||
|
/// If the membership is updated this field will be used to track to
|
||||||
|
/// original `origin_server_ts`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
|
||||||
|
/// A list of the foci in use for this membership.
|
||||||
|
pub foci_active: Vec<Focus>,
|
||||||
|
|
||||||
|
/// The id of the membership.
|
||||||
|
///
|
||||||
|
/// This is required to guarantee uniqueness of the event.
|
||||||
|
/// Sending the same state event twice to synapse makes the HS drop the second one and return
|
||||||
|
/// 200.
|
||||||
|
#[serde(rename = "membershipID")]
|
||||||
|
pub membership_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegacyMembershipData {
|
||||||
|
/// Checks if the event is expired.
|
||||||
|
///
|
||||||
|
/// Defaults to using `created_ts` of the [`LegacyMembershipData`].
|
||||||
|
/// If no `origin_server_ts` is provided and the event does not contain `created_ts`
|
||||||
|
/// the event will be considered as not expired.
|
||||||
|
/// In this case, a warning will be logged.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `origin_server_ts` - a fallback if [`LegacyMembershipData::created_ts`] is not present
|
||||||
|
pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
|
||||||
|
let ev_created_ts = self.created_ts.or(origin_server_ts);
|
||||||
|
|
||||||
|
if let Some(ev_created_ts) = ev_created_ts {
|
||||||
|
let now = MilliSecondsSinceUnixEpoch::now().to_system_time();
|
||||||
|
let expire_ts = ev_created_ts.to_system_time().map(|t| t + self.expires);
|
||||||
|
now > expire_ts
|
||||||
|
} else {
|
||||||
|
// This should not be reached since we only allow events that have copied over
|
||||||
|
// the origin server ts. `set_created_ts_if_none`
|
||||||
|
warn!("Encountered a Call Member state event where the origin_ts (or origin_server_ts) could not be found.\
|
||||||
|
It is treated as a non expired event but this might be wrong.");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initial set of fields of [`LegacyMembershipData`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(clippy::exhaustive_structs)]
|
||||||
|
pub struct LegacyMembershipDataInit {
|
||||||
|
/// The type of the MatrixRTC session the membership belongs to.
|
||||||
|
///
|
||||||
|
/// e.g. call, spacial, document...
|
||||||
|
pub application: Application,
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
///
|
||||||
|
/// The same user can join with their phone/computer.
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// The duration in milliseconds relative to the time this membership joined
|
||||||
|
/// during which the membership is valid.
|
||||||
|
///
|
||||||
|
/// The time a member has joined is defined as:
|
||||||
|
/// `MIN(content.created_ts, event.origin_server_ts)`
|
||||||
|
pub expires: Duration,
|
||||||
|
|
||||||
|
/// A list of the focuses (foci) in use for this membership.
|
||||||
|
pub foci_active: Vec<Focus>,
|
||||||
|
|
||||||
|
/// The id of the membership.
|
||||||
|
///
|
||||||
|
/// This is required to guarantee uniqueness of the event.
|
||||||
|
/// Sending the same state event twice to synapse makes the HS drop the second one and return
|
||||||
|
/// 200.
|
||||||
|
pub membership_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LegacyMembershipDataInit> for LegacyMembershipData {
|
||||||
|
fn from(init: LegacyMembershipDataInit) -> Self {
|
||||||
|
let LegacyMembershipDataInit {
|
||||||
|
application,
|
||||||
|
device_id,
|
||||||
|
expires,
|
||||||
|
foci_active,
|
||||||
|
membership_id,
|
||||||
|
} = init;
|
||||||
|
Self { application, device_id, expires, created_ts: None, foci_active, membership_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all the information for a MatrixRTC membership. (one for each device)
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct SessionMembershipData {
|
||||||
|
/// The type of the MatrixRTC session the membership belongs to.
|
||||||
|
///
|
||||||
|
/// e.g. call, spacial, document...
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub application: Application,
|
||||||
|
|
||||||
|
/// The device id of this membership.
|
||||||
|
///
|
||||||
|
/// The same user can join with their phone/computer.
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// A list of the foci that this membership proposes to use.
|
||||||
|
pub foci_preferred: Vec<Focus>,
|
||||||
|
|
||||||
|
/// Data required to determine the currently used focus by this member.
|
||||||
|
pub focus_active: ActiveFocus,
|
||||||
|
|
||||||
|
/// Stores a copy of the `origin_server_ts` of the initial session event.
|
||||||
|
///
|
||||||
|
/// This is not part of the serialized event and computed after serialization.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of the MatrixRTC session.
|
||||||
|
///
|
||||||
|
/// This is not the application/client used by the user but the
|
||||||
|
/// type of MatrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
|
||||||
|
/// possible applications.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "application")]
|
||||||
|
pub enum Application {
|
||||||
|
/// The rtc application (session type) for VoIP call.
|
||||||
|
#[serde(rename = "m.call")]
|
||||||
|
Call(CallApplicationContent),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call specific parameters of a `m.call.member` event.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct CallApplicationContent {
|
||||||
|
/// An identifier for calls.
|
||||||
|
///
|
||||||
|
/// All members using the same `call_id` will end up in the same call.
|
||||||
|
///
|
||||||
|
/// Does not need to be a uuid.
|
||||||
|
///
|
||||||
|
/// `""` is used for room scoped calls.
|
||||||
|
pub call_id: String,
|
||||||
|
|
||||||
|
/// Who owns/joins/controls (can modify) the call.
|
||||||
|
pub scope: CallScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CallApplicationContent {
|
||||||
|
/// Initialize a [`CallApplicationContent`].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
|
||||||
|
/// the same call. Does not need to be a uuid. `""` is used for room scoped calls.
|
||||||
|
/// * `scope` - Who owns/joins/controls (can modify) the call.
|
||||||
|
pub fn new(call_id: String, scope: CallScope) -> Self {
|
||||||
|
Self { call_id, scope }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The call scope defines different call ownership models.
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||||
|
#[derive(Clone, PartialEq, StringEnum)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_enum(rename_all = "m.snake_case")]
|
||||||
|
pub enum CallScope {
|
||||||
|
/// A call which every user of a room can join and create.
|
||||||
|
///
|
||||||
|
/// There is no particular name associated with it.
|
||||||
|
///
|
||||||
|
/// There can only be one per room.
|
||||||
|
Room,
|
||||||
|
|
||||||
|
/// A user call is owned by a user.
|
||||||
|
///
|
||||||
|
/// Each user can create one there can be multiple per room. They are started and ended by the
|
||||||
|
/// owning user.
|
||||||
|
User,
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
_Custom(PrivOwnedStr),
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
//! Type for the matrixRTC notify event ([MSC4075]).
|
//! Type for the MatrixRTC notify event ([MSC4075]).
|
||||||
//!
|
//!
|
||||||
//! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
|
//! [MSC4075]: https://github.com/matrix-org/matrix-spec-proposals/pull/4075
|
||||||
|
|
||||||
@ -75,3 +75,99 @@ impl From<Application> for ApplicationType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
|
||||||
|
Mentions,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notify_event_serialization() {
|
||||||
|
use ruma_common::owned_user_id;
|
||||||
|
|
||||||
|
let content_user_mention = CallNotifyEventContent::new(
|
||||||
|
"abcdef".into(),
|
||||||
|
ApplicationType::Call,
|
||||||
|
NotifyType::Ring,
|
||||||
|
Mentions::with_user_ids(vec![
|
||||||
|
owned_user_id!("@user:example.com"),
|
||||||
|
owned_user_id!("@user2:example.com"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let content_room_mention = CallNotifyEventContent::new(
|
||||||
|
"abcdef".into(),
|
||||||
|
ApplicationType::Call,
|
||||||
|
NotifyType::Ring,
|
||||||
|
Mentions::with_room_mention(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&content_user_mention).unwrap(),
|
||||||
|
json!({
|
||||||
|
"call_id": "abcdef",
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": {
|
||||||
|
"user_ids": ["@user2:example.com","@user:example.com"],
|
||||||
|
},
|
||||||
|
"notify_type": "ring",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&content_room_mention).unwrap(),
|
||||||
|
json!({
|
||||||
|
"call_id": "abcdef",
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": { "room": true },
|
||||||
|
"notify_type": "ring",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notify_event_deserialization() {
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use assert_matches2::assert_matches;
|
||||||
|
use ruma_common::owned_user_id;
|
||||||
|
|
||||||
|
use crate::{AnyMessageLikeEvent, MessageLikeEvent};
|
||||||
|
|
||||||
|
let json_data = json!({
|
||||||
|
"content": {
|
||||||
|
"call_id": "abcdef",
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": {
|
||||||
|
"room": false,
|
||||||
|
"user_ids": ["@user:example.com", "@user2:example.com"],
|
||||||
|
},
|
||||||
|
"notify_type": "ring",
|
||||||
|
},
|
||||||
|
"event_id": "$event:notareal.hs",
|
||||||
|
"origin_server_ts": 134_829_848,
|
||||||
|
"room_id": "!roomid:notareal.hs",
|
||||||
|
"sender": "@user:notareal.hs",
|
||||||
|
"type": "m.call.notify",
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
||||||
|
assert_matches!(
|
||||||
|
event,
|
||||||
|
AnyMessageLikeEvent::CallNotify(MessageLikeEvent::Original(message_event))
|
||||||
|
);
|
||||||
|
let content = message_event.content;
|
||||||
|
assert_eq!(content.call_id, "abcdef");
|
||||||
|
assert!(!content.mentions.room);
|
||||||
|
assert_eq!(
|
||||||
|
content.mentions.user_ids,
|
||||||
|
BTreeSet::from([
|
||||||
|
owned_user_id!("@user:example.com"),
|
||||||
|
owned_user_id!("@user2:example.com")
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! Types for the [`m.call.sdp_stream_metadata_changed`] event.
|
//! Types for the [`m.call.sdp_stream_metadata_changed`] event.
|
||||||
//!
|
//!
|
||||||
//! [`m.call.sdp_stream_metadata_changed`]: https://github.com/matrix-org/matrix-spec-proposals/pull/3291
|
//! [`m.call.sdp_stream_metadata_changed`]: https://spec.matrix.org/latest/client-server-api/#mcallsdp_stream_metadata_changed
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ use super::StreamMetadata;
|
|||||||
/// This event is sent by any party when a stream metadata changes but no negotiation is required.
|
/// This event is sent by any party when a stream metadata changes but no negotiation is required.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_event(type = "org.matrix.call.sdp_stream_metadata_changed", kind = MessageLike)]
|
#[ruma_event(type = "m.call.sdp_stream_metadata_changed", alias = "org.matrix.call.sdp_stream_metadata_changed", kind = MessageLike)]
|
||||||
pub struct CallSdpStreamMetadataChangedEventContent {
|
pub struct CallSdpStreamMetadataChangedEventContent {
|
||||||
/// A unique identifier for the call.
|
/// A unique identifier for the call.
|
||||||
pub call_id: OwnedVoipId,
|
pub call_id: OwnedVoipId,
|
||||||
|
@ -95,7 +95,6 @@ pub trait ToDeviceEventContent: EventContent<EventType = ToDeviceEventType> {}
|
|||||||
/// Event content that can be deserialized with its event type.
|
/// Event content that can be deserialized with its event type.
|
||||||
pub trait EventContentFromType: EventContent {
|
pub trait EventContentFromType: EventContent {
|
||||||
/// Constructs this event content from the given event type and JSON.
|
/// Constructs this event content from the given event type and JSON.
|
||||||
#[doc(hidden)]
|
|
||||||
fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self>;
|
fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +45,8 @@ event_enum! {
|
|||||||
"m.call.candidates" => super::call::candidates,
|
"m.call.candidates" => super::call::candidates,
|
||||||
"m.call.negotiate" => super::call::negotiate,
|
"m.call.negotiate" => super::call::negotiate,
|
||||||
"m.call.reject" => super::call::reject,
|
"m.call.reject" => super::call::reject,
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
#[ruma_enum(alias = "org.matrix.call.sdp_stream_metadata_changed")]
|
||||||
#[ruma_enum(alias = "m.call.sdp_stream_metadata_changed")]
|
"m.call.sdp_stream_metadata_changed" => super::call::sdp_stream_metadata_changed,
|
||||||
"org.matrix.call.sdp_stream_metadata_changed" => super::call::sdp_stream_metadata_changed,
|
|
||||||
"m.call.select_answer" => super::call::select_answer,
|
"m.call.select_answer" => super::call::select_answer,
|
||||||
#[cfg(feature = "unstable-msc3954")]
|
#[cfg(feature = "unstable-msc3954")]
|
||||||
#[ruma_enum(alias = "m.emote")]
|
#[ruma_enum(alias = "m.emote")]
|
||||||
@ -88,6 +87,9 @@ event_enum! {
|
|||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
#[ruma_enum(ident = UnstablePollEnd)]
|
#[ruma_enum(ident = UnstablePollEnd)]
|
||||||
"org.matrix.msc3381.poll.end" => super::poll::unstable_end,
|
"org.matrix.msc3381.poll.end" => super::poll::unstable_end,
|
||||||
|
#[cfg(feature = "unstable-msc3489")]
|
||||||
|
#[ruma_enum(alias = "m.beacon")]
|
||||||
|
"org.matrix.msc3672.beacon" => super::beacon,
|
||||||
"m.reaction" => super::reaction,
|
"m.reaction" => super::reaction,
|
||||||
"m.room.encrypted" => super::room::encrypted,
|
"m.room.encrypted" => super::room::encrypted,
|
||||||
"m.room.message" => super::room::message,
|
"m.room.message" => super::room::message,
|
||||||
@ -127,6 +129,9 @@ event_enum! {
|
|||||||
"m.room.topic" => super::room::topic,
|
"m.room.topic" => super::room::topic,
|
||||||
"m.space.child" => super::space::child,
|
"m.space.child" => super::space::child,
|
||||||
"m.space.parent" => super::space::parent,
|
"m.space.parent" => super::space::parent,
|
||||||
|
#[cfg(feature = "unstable-msc3489")]
|
||||||
|
#[ruma_enum(alias = "m.beacon_info")]
|
||||||
|
"org.matrix.msc3672.beacon_info" => super::beacon_info,
|
||||||
#[cfg(feature = "unstable-msc3401")]
|
#[cfg(feature = "unstable-msc3401")]
|
||||||
#[ruma_enum(alias = "m.call.member")]
|
#[ruma_enum(alias = "m.call.member")]
|
||||||
"org.matrix.msc3401.call.member" => super::call::member,
|
"org.matrix.msc3401.call.member" => super::call::member,
|
||||||
@ -309,6 +314,8 @@ impl AnyMessageLikeEventContent {
|
|||||||
/// This is a helper function intended for encryption. There should not be a reason to access
|
/// This is a helper function intended for encryption. There should not be a reason to access
|
||||||
/// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise.
|
/// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise.
|
||||||
pub fn relation(&self) -> Option<encrypted::Relation> {
|
pub fn relation(&self) -> Option<encrypted::Relation> {
|
||||||
|
#[cfg(feature = "unstable-msc3489")]
|
||||||
|
use super::beacon::BeaconEventContent;
|
||||||
use super::key::verification::{
|
use super::key::verification::{
|
||||||
accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent,
|
accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent,
|
||||||
done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent,
|
done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent,
|
||||||
@ -361,13 +368,16 @@ impl AnyMessageLikeEventContent {
|
|||||||
| Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => {
|
| Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => {
|
||||||
Some(encrypted::Relation::Reference(relates_to.clone()))
|
Some(encrypted::Relation::Reference(relates_to.clone()))
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "unstable-msc3489")]
|
||||||
|
Self::Beacon(BeaconEventContent { relates_to, .. }) => {
|
||||||
|
Some(encrypted::Relation::Reference(relates_to.clone()))
|
||||||
|
}
|
||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
Self::PollStart(_) | Self::UnstablePollStart(_) => None,
|
Self::PollStart(_) | Self::UnstablePollStart(_) => None,
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
#[cfg(feature = "unstable-msc4075")]
|
||||||
Self::CallNotify(_) => None,
|
Self::CallNotify(_) => None,
|
||||||
#[cfg(feature = "unstable-msc3291")]
|
Self::CallSdpStreamMetadataChanged(_)
|
||||||
Self::CallSdpStreamMetadataChanged(_) => None,
|
| Self::CallNegotiate(_)
|
||||||
Self::CallNegotiate(_)
|
|
||||||
| Self::CallReject(_)
|
| Self::CallReject(_)
|
||||||
| Self::CallSelectAnswer(_)
|
| Self::CallSelectAnswer(_)
|
||||||
| Self::CallAnswer(_)
|
| Self::CallAnswer(_)
|
||||||
|
@ -165,7 +165,10 @@ mod tests {
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use ruma_common::{event_id, serde::Base64};
|
use ruma_common::{
|
||||||
|
event_id,
|
||||||
|
serde::{Base64, Raw},
|
||||||
|
};
|
||||||
use serde_json::{
|
use serde_json::{
|
||||||
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
||||||
};
|
};
|
||||||
@ -348,4 +351,26 @@ mod tests {
|
|||||||
assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
|
assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
|
||||||
assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
|
assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn in_room_serialization_roundtrip() {
|
||||||
|
let event_id = event_id!("$1598361704261elfgc:localhost");
|
||||||
|
|
||||||
|
let content = KeyVerificationAcceptEventContent {
|
||||||
|
relates_to: Reference { event_id: event_id.to_owned() },
|
||||||
|
method: AcceptMethod::SasV1(SasV1Content {
|
||||||
|
hash: HashAlgorithm::Sha256,
|
||||||
|
key_agreement_protocol: KeyAgreementProtocol::Curve25519,
|
||||||
|
message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
|
||||||
|
short_authentication_string: vec![ShortAuthenticationString::Decimal],
|
||||||
|
commitment: Base64::new(b"hello".to_vec()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.method, AcceptMethod::SasV1(_));
|
||||||
|
assert_eq!(deser_content.relates_to.event_id, event_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,10 @@ pub mod macros {
|
|||||||
|
|
||||||
#[cfg(feature = "unstable-msc3927")]
|
#[cfg(feature = "unstable-msc3927")]
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
|
#[cfg(feature = "unstable-msc3489")]
|
||||||
|
pub mod beacon;
|
||||||
|
#[cfg(feature = "unstable-msc3489")]
|
||||||
|
pub mod beacon_info;
|
||||||
pub mod call;
|
pub mod call;
|
||||||
pub mod direct;
|
pub mod direct;
|
||||||
pub mod dummy;
|
pub mod dummy;
|
||||||
|
@ -37,7 +37,7 @@ impl From<Annotation> for ReactionEventContent {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use ruma_common::owned_event_id;
|
use ruma_common::{owned_event_id, serde::Raw};
|
||||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||||
|
|
||||||
use super::ReactionEventContent;
|
use super::ReactionEventContent;
|
||||||
@ -79,4 +79,18 @@ mod tests {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialization_roundtrip() {
|
||||||
|
let content = ReactionEventContent::new(Annotation::new(
|
||||||
|
owned_event_id!("$my_reaction"),
|
||||||
|
"🏠".to_owned(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(deser_content.relates_to.event_id, content.relates_to.event_id);
|
||||||
|
assert_eq!(deser_content.relates_to.key, content.relates_to.key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,7 @@ impl<C> From<message::Relation<C>> for Relation {
|
|||||||
/// [replaces another event]: https://spec.matrix.org/latest/client-server-api/#event-replacements
|
/// [replaces another event]: https://spec.matrix.org/latest/client-server-api/#event-replacements
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[serde(tag = "rel_type", rename = "m.replace")]
|
||||||
pub struct Replacement {
|
pub struct Replacement {
|
||||||
/// The ID of the event being replaced.
|
/// The ID of the event being replaced.
|
||||||
pub event_id: OwnedEventId,
|
pub event_id: OwnedEventId,
|
||||||
|
@ -74,27 +74,11 @@ impl Serialize for Relation {
|
|||||||
st.serialize_field("m.in_reply_to", in_reply_to)?;
|
st.serialize_field("m.in_reply_to", in_reply_to)?;
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
Relation::Replacement(data) => {
|
Relation::Replacement(data) => data.serialize(serializer),
|
||||||
RelationSerHelper { rel_type: "m.replace", data }.serialize(serializer)
|
Relation::Reference(data) => data.serialize(serializer),
|
||||||
}
|
Relation::Annotation(data) => data.serialize(serializer),
|
||||||
Relation::Reference(data) => {
|
Relation::Thread(data) => data.serialize(serializer),
|
||||||
RelationSerHelper { rel_type: "m.reference", data }.serialize(serializer)
|
|
||||||
}
|
|
||||||
Relation::Annotation(data) => {
|
|
||||||
RelationSerHelper { rel_type: "m.annotation", data }.serialize(serializer)
|
|
||||||
}
|
|
||||||
Relation::Thread(data) => {
|
|
||||||
RelationSerHelper { rel_type: "m.thread", data }.serialize(serializer)
|
|
||||||
}
|
|
||||||
Relation::_Custom(c) => c.serialize(serializer),
|
Relation::_Custom(c) => c.serialize(serializer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct RelationSerHelper<'a, T> {
|
|
||||||
rel_type: &'a str,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
data: &'a T,
|
|
||||||
}
|
|
||||||
|
@ -168,6 +168,7 @@ impl From<JoinRule> for SpaceRoomJoinRule {
|
|||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
pub struct Restricted {
|
pub struct Restricted {
|
||||||
/// Allow rules which describe conditions that allow joining a room.
|
/// Allow rules which describe conditions that allow joining a room.
|
||||||
|
#[serde(default)]
|
||||||
pub allow: Vec<AllowRule>,
|
pub allow: Vec<AllowRule>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +325,16 @@ mod tests {
|
|||||||
assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
|
assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restricted_room_no_allow_field() {
|
||||||
|
let json = r#"{"join_rule":"restricted"}"#;
|
||||||
|
let join_rules: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
|
||||||
|
assert_matches!(
|
||||||
|
join_rules,
|
||||||
|
RoomJoinRulesEventContent { join_rule: JoinRule::Restricted(_) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn join_rule_to_space_room_join_rule() {
|
fn join_rule_to_space_room_join_rule() {
|
||||||
assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into());
|
assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into());
|
||||||
|
@ -157,7 +157,7 @@ pub(super) enum RelationSerHelper {
|
|||||||
Replacement(ReplacementJsonRepr),
|
Replacement(ReplacementJsonRepr),
|
||||||
|
|
||||||
/// An event that belongs to a thread, with stable names.
|
/// An event that belongs to a thread, with stable names.
|
||||||
#[serde(rename = "m.thread")]
|
#[serde(untagged)]
|
||||||
Thread(Thread),
|
Thread(Thread),
|
||||||
|
|
||||||
/// An unknown relation type.
|
/// An unknown relation type.
|
||||||
|
@ -57,7 +57,7 @@ fn is_default_bits(val: &UInt) -> bool {
|
|||||||
///
|
///
|
||||||
/// The only algorithm currently specified is `m.secret_storage.v1.aes-hmac-sha2`, so this
|
/// The only algorithm currently specified is `m.secret_storage.v1.aes-hmac-sha2`, so this
|
||||||
/// essentially represents `AesHmacSha2KeyDescription` in the
|
/// essentially represents `AesHmacSha2KeyDescription` in the
|
||||||
/// [spec](https://spec.matrix.org/latest/client-server-api/#msecret_storagev1aes-hmac-sha2).
|
/// [spec](https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2).
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[derive(Clone, Debug, Serialize, EventContent)]
|
#[derive(Clone, Debug, Serialize, EventContent)]
|
||||||
#[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)]
|
#[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)]
|
||||||
@ -137,7 +137,7 @@ impl SecretStorageEncryptionAlgorithm {
|
|||||||
/// The key properties for the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
|
/// The key properties for the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
|
||||||
///
|
///
|
||||||
/// Corresponds to the AES-specific properties of `AesHmacSha2KeyDescription` in the
|
/// Corresponds to the AES-specific properties of `AesHmacSha2KeyDescription` in the
|
||||||
/// [spec](https://spec.matrix.org/latest/client-server-api/#msecret_storagev1aes-hmac-sha2).
|
/// [spec](https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2).
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
pub struct SecretStorageV1AesHmacSha2Properties {
|
pub struct SecretStorageV1AesHmacSha2Properties {
|
||||||
@ -182,7 +182,7 @@ mod tests {
|
|||||||
PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
|
PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
|
||||||
SecretStorageV1AesHmacSha2Properties,
|
SecretStorageV1AesHmacSha2Properties,
|
||||||
};
|
};
|
||||||
use crate::{EventContentFromType, GlobalAccountDataEvent};
|
use crate::{AnyGlobalAccountDataEvent, EventContentFromType, GlobalAccountDataEvent};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn key_description_serialization() {
|
fn key_description_serialization() {
|
||||||
@ -326,7 +326,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_serialization() {
|
fn event_content_serialization() {
|
||||||
let mut content = SecretStorageKeyEventContent::new(
|
let mut content = SecretStorageKeyEventContent::new(
|
||||||
"my_key_id".into(),
|
"my_key_id".into(),
|
||||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
|
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
|
||||||
@ -346,6 +346,31 @@ mod tests {
|
|||||||
assert_eq!(to_json_value(&content).unwrap(), json);
|
assert_eq!(to_json_value(&content).unwrap(), json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_serialization() {
|
||||||
|
let mut content = SecretStorageKeyEventContent::new(
|
||||||
|
"my_key_id".into(),
|
||||||
|
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
|
||||||
|
iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
|
||||||
|
mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
content.name = Some("my_key".to_owned());
|
||||||
|
let event = GlobalAccountDataEvent { content };
|
||||||
|
|
||||||
|
let json = json!({
|
||||||
|
"type": "m.secret_storage.key.my_key_id",
|
||||||
|
"content": {
|
||||||
|
"name": "my_key",
|
||||||
|
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||||
|
"iv": "YWJjZGVmZ2hpamtsbW5vcA",
|
||||||
|
"mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(to_json_value(&event).unwrap(), json);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_deserialization() {
|
fn event_deserialization() {
|
||||||
let json = json!({
|
let json = json!({
|
||||||
@ -358,8 +383,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let ev =
|
let any_ev = from_json_value::<AnyGlobalAccountDataEvent>(json).unwrap();
|
||||||
from_json_value::<GlobalAccountDataEvent<SecretStorageKeyEventContent>>(json).unwrap();
|
assert_matches!(any_ev, AnyGlobalAccountDataEvent::SecretStorageKey(ev));
|
||||||
assert_eq!(ev.content.key_id, "my_key_id");
|
assert_eq!(ev.content.key_id, "my_key_id");
|
||||||
assert_eq!(ev.content.name.unwrap(), "my_key");
|
assert_eq!(ev.content.name.unwrap(), "my_key");
|
||||||
assert_matches!(ev.content.passphrase, None);
|
assert_matches!(ev.content.passphrase, None);
|
||||||
|
81
crates/ruma-events/tests/it/beacon.rs
Normal file
81
crates/ruma-events/tests/it/beacon.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#![cfg(feature = "unstable-msc3489")]
|
||||||
|
|
||||||
|
use assert_matches2::assert_matches;
|
||||||
|
use js_int::uint;
|
||||||
|
use ruma_common::{
|
||||||
|
owned_event_id, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch,
|
||||||
|
};
|
||||||
|
use ruma_events::{
|
||||||
|
beacon::BeaconEventContent, relation::Reference, AnyMessageLikeEvent, MessageLikeEvent,
|
||||||
|
};
|
||||||
|
use serde_json::{
|
||||||
|
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_beacon_event_content() -> BeaconEventContent {
|
||||||
|
BeaconEventContent::new(
|
||||||
|
owned_event_id!("$beacon_info_event_id:example.com"),
|
||||||
|
"geo:51.5008,0.1247;u=35".to_owned(),
|
||||||
|
Some(MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_beacon_event_content_json() -> JsonValue {
|
||||||
|
json!({
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$beacon_info_event_id:example.com"
|
||||||
|
},
|
||||||
|
"org.matrix.msc3488.location": {
|
||||||
|
"uri": "geo:51.5008,0.1247;u=35",
|
||||||
|
},
|
||||||
|
"org.matrix.msc3488.ts": 1_636_829_458
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_event_content_serialization() {
|
||||||
|
let event_content = get_beacon_event_content();
|
||||||
|
|
||||||
|
assert_eq!(to_json_value(&event_content).unwrap(), get_beacon_event_content_json());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_event_content_deserialization() {
|
||||||
|
let json_data = get_beacon_event_content_json();
|
||||||
|
|
||||||
|
let event_content: BeaconEventContent =
|
||||||
|
from_json_value::<BeaconEventContent>(json_data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
event_content.relates_to.event_id,
|
||||||
|
owned_event_id!("$beacon_info_event_id:example.com")
|
||||||
|
);
|
||||||
|
assert_eq!(event_content.location.uri, "geo:51.5008,0.1247;u=35");
|
||||||
|
assert_eq!(event_content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_event_deserialization() {
|
||||||
|
let json_data = json!({
|
||||||
|
"content": get_beacon_event_content_json(),
|
||||||
|
"event_id": "$beacon_event_id:example.com",
|
||||||
|
"origin_server_ts": 1_636_829_458,
|
||||||
|
"room_id": "!roomid:example.com",
|
||||||
|
"type": "org.matrix.msc3672.beacon",
|
||||||
|
"sender": "@example:example.com"
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(event, AnyMessageLikeEvent::Beacon(MessageLikeEvent::Original(ev)));
|
||||||
|
assert_eq!(ev.content.location.uri, "geo:51.5008,0.1247;u=35");
|
||||||
|
assert_eq!(ev.content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
|
||||||
|
assert_matches!(ev.content.relates_to, Reference { event_id, .. });
|
||||||
|
assert_eq!(event_id, owned_event_id!("$beacon_info_event_id:example.com"));
|
||||||
|
|
||||||
|
assert_eq!(ev.sender, user_id!("@example:example.com"));
|
||||||
|
assert_eq!(ev.room_id, room_id!("!roomid:example.com"));
|
||||||
|
assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
|
||||||
|
assert!(ev.unsigned.is_empty());
|
||||||
|
}
|
161
crates/ruma-events/tests/it/beacon_info.rs
Normal file
161
crates/ruma-events/tests/it/beacon_info.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
#![cfg(feature = "unstable-msc3489")]
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use assert_matches2::assert_matches;
|
||||||
|
use js_int::uint;
|
||||||
|
use ruma_common::{event_id, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch};
|
||||||
|
use ruma_events::{
|
||||||
|
beacon_info::BeaconInfoEventContent, location::AssetType, AnyStateEvent, StateEvent,
|
||||||
|
};
|
||||||
|
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||||
|
|
||||||
|
fn get_beacon_info_event_content(
|
||||||
|
duration: Option<Duration>,
|
||||||
|
ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> BeaconInfoEventContent {
|
||||||
|
let description = Some("Kylie's live location".to_owned());
|
||||||
|
let duration_or = duration.unwrap_or(Duration::from_secs(60));
|
||||||
|
let ts_or = Some(ts.unwrap_or(MilliSecondsSinceUnixEpoch::now()));
|
||||||
|
|
||||||
|
BeaconInfoEventContent::new(description, duration_or, true, ts_or)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_beacon_info_json() -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc3488.ts": 1_636_829_458,
|
||||||
|
"org.matrix.msc3488.asset": {
|
||||||
|
"type": "m.self"
|
||||||
|
},
|
||||||
|
"timeout": 60_000,
|
||||||
|
"description": "Kylie's live location",
|
||||||
|
"live": true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_info_is_live() {
|
||||||
|
let event_content = get_beacon_info_event_content(None, None);
|
||||||
|
|
||||||
|
assert!(event_content.is_live());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_info_is_not_live() {
|
||||||
|
let duration = Some(Duration::from_nanos(1));
|
||||||
|
let event_content = get_beacon_info_event_content(duration, None);
|
||||||
|
|
||||||
|
assert!(!event_content.is_live());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_info_stop_event() {
|
||||||
|
let ts = Some(MilliSecondsSinceUnixEpoch(1_636_829_458_u64.try_into().unwrap()));
|
||||||
|
|
||||||
|
let mut event_content = get_beacon_info_event_content(None, ts);
|
||||||
|
|
||||||
|
event_content.stop();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&event_content).unwrap(),
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc3488.ts": 1_636_829_458,
|
||||||
|
"org.matrix.msc3488.asset": {
|
||||||
|
"type": "m.self"
|
||||||
|
},
|
||||||
|
"timeout": 60_000,
|
||||||
|
"description": "Kylie's live location",
|
||||||
|
"live": false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_info_start_event() {
|
||||||
|
let ts = Some(MilliSecondsSinceUnixEpoch(1_636_829_458_u64.try_into().unwrap()));
|
||||||
|
|
||||||
|
let mut event_content = BeaconInfoEventContent::new(
|
||||||
|
Some("Kylie's live location".to_owned()),
|
||||||
|
Duration::from_secs(60),
|
||||||
|
false,
|
||||||
|
ts,
|
||||||
|
);
|
||||||
|
|
||||||
|
event_content.start();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&event_content).unwrap(),
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc3488.ts": 1_636_829_458,
|
||||||
|
"org.matrix.msc3488.asset": {
|
||||||
|
"type": "m.self"
|
||||||
|
},
|
||||||
|
"timeout": 60_000,
|
||||||
|
"description": "Kylie's live location",
|
||||||
|
"live": true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_info_start_event_content_serialization() {
|
||||||
|
let ts = Some(MilliSecondsSinceUnixEpoch(1_636_829_458_u64.try_into().unwrap()));
|
||||||
|
|
||||||
|
let event_content = get_beacon_info_event_content(None, ts);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&event_content).unwrap(),
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc3488.ts": 1_636_829_458,
|
||||||
|
"org.matrix.msc3488.asset": {
|
||||||
|
"type": "m.self"
|
||||||
|
},
|
||||||
|
"timeout": 60_000,
|
||||||
|
"description": "Kylie's live location",
|
||||||
|
"live": true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beacon_info_start_event_content_deserialization() {
|
||||||
|
let json_data = get_beacon_info_json();
|
||||||
|
|
||||||
|
let event_content: BeaconInfoEventContent = serde_json::from_value(json_data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(event_content.description, Some("Kylie's live location".to_owned()));
|
||||||
|
assert!(event_content.live);
|
||||||
|
assert_eq!(event_content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
|
||||||
|
assert_eq!(event_content.timeout, Duration::from_secs(60));
|
||||||
|
assert_eq!(event_content.asset.type_, AssetType::Self_);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_event_deserialization() {
|
||||||
|
let json_data = json!({
|
||||||
|
"content": get_beacon_info_json(),
|
||||||
|
"event_id": "$beacon_event_id:example.com",
|
||||||
|
"origin_server_ts": 1_636_829_458,
|
||||||
|
"room_id": "!roomid:example.com",
|
||||||
|
"type": "org.matrix.msc3672.beacon_info",
|
||||||
|
"sender": "@example:example.com",
|
||||||
|
"state_key": "@example:example.com"
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyStateEvent>(json_data).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(event, AnyStateEvent::BeaconInfo(StateEvent::Original(ev)));
|
||||||
|
|
||||||
|
assert_eq!(ev.content.description, Some("Kylie's live location".to_owned()));
|
||||||
|
assert_eq!(ev.content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
|
||||||
|
assert_eq!(ev.content.timeout, Duration::from_secs(60));
|
||||||
|
assert_eq!(ev.content.asset.type_, AssetType::Self_);
|
||||||
|
assert!(ev.content.live);
|
||||||
|
|
||||||
|
assert_eq!(ev.event_id, event_id!("$beacon_event_id:example.com"));
|
||||||
|
assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
|
||||||
|
assert_eq!(ev.room_id, room_id!("!roomid:example.com"));
|
||||||
|
assert_eq!(ev.sender, user_id!("@example:example.com"));
|
||||||
|
assert_eq!(ev.state_key, "@example:example.com");
|
||||||
|
assert!(ev.unsigned.is_empty());
|
||||||
|
}
|
@ -1,6 +1,3 @@
|
|||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
use assign::assign;
|
use assign::assign;
|
||||||
@ -8,11 +5,6 @@ use js_int::uint;
|
|||||||
use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
use ruma_common::{room_id, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
use ruma_events::call::CallCapabilities;
|
use ruma_events::call::CallCapabilities;
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
use ruma_events::{
|
|
||||||
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
|
|
||||||
Mentions,
|
|
||||||
};
|
|
||||||
use ruma_events::{
|
use ruma_events::{
|
||||||
call::{
|
call::{
|
||||||
answer::CallAnswerEventContent,
|
answer::CallAnswerEventContent,
|
||||||
@ -616,83 +608,3 @@ fn select_v1_answer_event_deserialization() {
|
|||||||
assert_eq!(content.selected_party_id, "6336");
|
assert_eq!(content.selected_party_id, "6336");
|
||||||
assert_eq!(content.version, VoipVersionId::V1);
|
assert_eq!(content.version, VoipVersionId::V1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
#[test]
|
|
||||||
fn notify_event_serialization() {
|
|
||||||
use ruma_common::owned_user_id;
|
|
||||||
|
|
||||||
let content_user_mention = CallNotifyEventContent::new(
|
|
||||||
"abcdef".into(),
|
|
||||||
ApplicationType::Call,
|
|
||||||
NotifyType::Ring,
|
|
||||||
Mentions::with_user_ids(vec![
|
|
||||||
owned_user_id!("@user:example.com"),
|
|
||||||
owned_user_id!("@user2:example.com"),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let content_room_mention = CallNotifyEventContent::new(
|
|
||||||
"abcdef".into(),
|
|
||||||
ApplicationType::Call,
|
|
||||||
NotifyType::Ring,
|
|
||||||
Mentions::with_room_mention(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
to_json_value(&content_user_mention).unwrap(),
|
|
||||||
json!({
|
|
||||||
"call_id": "abcdef",
|
|
||||||
"application": "m.call",
|
|
||||||
"m.mentions": {
|
|
||||||
"user_ids": ["@user2:example.com","@user:example.com"],
|
|
||||||
},
|
|
||||||
"notify_type": "ring",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
to_json_value(&content_room_mention).unwrap(),
|
|
||||||
json!({
|
|
||||||
"call_id": "abcdef",
|
|
||||||
"application": "m.call",
|
|
||||||
"m.mentions": { "room": true },
|
|
||||||
"notify_type": "ring",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc4075")]
|
|
||||||
#[test]
|
|
||||||
fn notify_event_deserialization() {
|
|
||||||
use ruma_common::owned_user_id;
|
|
||||||
|
|
||||||
let json_data = json!({
|
|
||||||
"content": {
|
|
||||||
"call_id": "abcdef",
|
|
||||||
"application": "m.call",
|
|
||||||
"m.mentions": {
|
|
||||||
"room": false,
|
|
||||||
"user_ids": ["@user:example.com", "@user2:example.com"],
|
|
||||||
},
|
|
||||||
"notify_type": "ring",
|
|
||||||
},
|
|
||||||
"event_id": "$event:notareal.hs",
|
|
||||||
"origin_server_ts": 134_829_848,
|
|
||||||
"room_id": "!roomid:notareal.hs",
|
|
||||||
"sender": "@user:notareal.hs",
|
|
||||||
"type": "m.call.notify",
|
|
||||||
});
|
|
||||||
|
|
||||||
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
|
||||||
assert_matches!(
|
|
||||||
event,
|
|
||||||
AnyMessageLikeEvent::CallNotify(MessageLikeEvent::Original(message_event))
|
|
||||||
);
|
|
||||||
let content = message_event.content;
|
|
||||||
assert_eq!(content.call_id, "abcdef");
|
|
||||||
assert!(!content.mentions.room);
|
|
||||||
assert_eq!(
|
|
||||||
content.mentions.user_ids,
|
|
||||||
BTreeSet::from([owned_user_id!("@user:example.com"), owned_user_id!("@user2:example.com")])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use ruma_common::{owned_device_id, owned_event_id};
|
use ruma_common::{owned_device_id, owned_event_id, serde::Raw};
|
||||||
use ruma_events::{
|
use ruma_events::{
|
||||||
relation::{CustomRelation, InReplyTo, Reference, Thread},
|
relation::{Annotation, CustomRelation, InReplyTo, Reference, Thread},
|
||||||
room::encrypted::{
|
room::encrypted::{
|
||||||
EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement,
|
EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement,
|
||||||
RoomEncryptedEventContent,
|
RoomEncryptedEventContent,
|
||||||
@ -82,6 +82,17 @@ fn content_no_relation_deserialization() {
|
|||||||
assert_matches!(content.relates_to, None);
|
assert_matches!(content.relates_to, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn content_no_relation_serialization_roundtrip() {
|
||||||
|
let content = RoomEncryptedEventContent::new(encrypted_scheme(), None);
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
assert_matches!(deser_content.relates_to, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn content_reply_serialization() {
|
fn content_reply_serialization() {
|
||||||
let content = RoomEncryptedEventContent::new(
|
let content = RoomEncryptedEventContent::new(
|
||||||
@ -149,6 +160,22 @@ fn content_reply_deserialization() {
|
|||||||
assert_eq!(in_reply_to.event_id, "$replied_to_event");
|
assert_eq!(in_reply_to.event_id, "$replied_to_event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn content_reply_serialization_roundtrip() {
|
||||||
|
let reply = InReplyTo::new(owned_event_id!("$replied_to_event"));
|
||||||
|
let content = RoomEncryptedEventContent::new(
|
||||||
|
encrypted_scheme(),
|
||||||
|
Some(Relation::Reply { in_reply_to: reply.clone() }),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
assert_matches!(deser_content.relates_to, Some(Relation::Reply { in_reply_to: deser_reply }));
|
||||||
|
assert_eq!(deser_reply.event_id, reply.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn content_replacement_serialization() {
|
fn content_replacement_serialization() {
|
||||||
let content = RoomEncryptedEventContent::new(
|
let content = RoomEncryptedEventContent::new(
|
||||||
@ -214,6 +241,22 @@ fn content_replacement_deserialization() {
|
|||||||
assert_eq!(replacement.event_id, "$replaced_event");
|
assert_eq!(replacement.event_id, "$replaced_event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn content_replacement_serialization_roundtrip() {
|
||||||
|
let replacement = Replacement::new(owned_event_id!("$replaced_event"));
|
||||||
|
let content = RoomEncryptedEventContent::new(
|
||||||
|
encrypted_scheme(),
|
||||||
|
Some(Relation::Replacement(replacement.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
assert_matches!(deser_content.relates_to, Some(Relation::Replacement(deser_replacement)));
|
||||||
|
assert_eq!(deser_replacement.event_id, replacement.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn content_reference_serialization() {
|
fn content_reference_serialization() {
|
||||||
let content = RoomEncryptedEventContent::new(
|
let content = RoomEncryptedEventContent::new(
|
||||||
@ -279,6 +322,22 @@ fn content_reference_deserialization() {
|
|||||||
assert_eq!(reference.event_id, "$referenced_event");
|
assert_eq!(reference.event_id, "$referenced_event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn content_reference_serialization_roundtrip() {
|
||||||
|
let reference = Reference::new(owned_event_id!("$referenced_event"));
|
||||||
|
let content = RoomEncryptedEventContent::new(
|
||||||
|
encrypted_scheme(),
|
||||||
|
Some(Relation::Reference(reference.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
assert_matches!(deser_content.relates_to, Some(Relation::Reference(deser_reference)));
|
||||||
|
assert_eq!(deser_reference.event_id, reference.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn content_thread_serialization() {
|
fn content_thread_serialization() {
|
||||||
let content = RoomEncryptedEventContent::new(
|
let content = RoomEncryptedEventContent::new(
|
||||||
@ -357,9 +416,23 @@ fn content_thread_deserialization() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn content_annotation_serialization() {
|
fn content_thread_serialization_roundtrip() {
|
||||||
use ruma_events::relation::Annotation;
|
let thread = Thread::plain(owned_event_id!("$thread_root"), owned_event_id!("$prev_event"));
|
||||||
|
let content =
|
||||||
|
RoomEncryptedEventContent::new(encrypted_scheme(), Some(Relation::Thread(thread.clone())));
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
assert_matches!(deser_content.relates_to, Some(Relation::Thread(deser_thread)));
|
||||||
|
assert_eq!(deser_thread.event_id, thread.event_id);
|
||||||
|
assert_eq!(deser_thread.in_reply_to.unwrap().event_id, thread.in_reply_to.unwrap().event_id);
|
||||||
|
assert_eq!(deser_thread.is_falling_back, thread.is_falling_back);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn content_annotation_serialization() {
|
||||||
let content = RoomEncryptedEventContent::new(
|
let content = RoomEncryptedEventContent::new(
|
||||||
encrypted_scheme(),
|
encrypted_scheme(),
|
||||||
Some(Relation::Annotation(Annotation::new(
|
Some(Relation::Annotation(Annotation::new(
|
||||||
@ -429,6 +502,23 @@ fn content_annotation_deserialization() {
|
|||||||
assert_eq!(annotation.key, "some_key");
|
assert_eq!(annotation.key, "some_key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn content_annotation_serialization_roundtrip() {
|
||||||
|
let annotation = Annotation::new(owned_event_id!("$annotated_event"), "some_key".to_owned());
|
||||||
|
let content = RoomEncryptedEventContent::new(
|
||||||
|
encrypted_scheme(),
|
||||||
|
Some(Relation::Annotation(annotation.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
assert_matches!(deser_content.relates_to, Some(Relation::Annotation(deser_annotation)));
|
||||||
|
assert_eq!(deser_annotation.event_id, annotation.event_id);
|
||||||
|
assert_eq!(deser_annotation.key, annotation.key);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_relation_deserialization() {
|
fn custom_relation_deserialization() {
|
||||||
let relation_json = json!({
|
let relation_json = json!({
|
||||||
@ -502,3 +592,31 @@ fn custom_relation_serialization() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_serialization_roundtrip() {
|
||||||
|
let rel_type = "io.ruma.unknown";
|
||||||
|
let event_id = "$related_event";
|
||||||
|
let key = "value";
|
||||||
|
let json_relation = json!({
|
||||||
|
"rel_type": rel_type,
|
||||||
|
"event_id": event_id,
|
||||||
|
"key": key,
|
||||||
|
});
|
||||||
|
let relation = from_json_value::<CustomRelation>(json_relation).unwrap();
|
||||||
|
|
||||||
|
let content =
|
||||||
|
RoomEncryptedEventContent::new(encrypted_scheme(), Some(Relation::_Custom(relation)));
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(content.scheme, EncryptedEventScheme::MegolmV1AesSha2(_));
|
||||||
|
let deser_relates_to = deser_content.relates_to.unwrap();
|
||||||
|
assert_matches!(&deser_relates_to, Relation::_Custom(_));
|
||||||
|
assert_eq!(deser_relates_to.rel_type().unwrap().as_str(), rel_type);
|
||||||
|
let deser_relation = deser_relates_to.data();
|
||||||
|
assert_eq!(deser_relation.get("rel_type").unwrap().as_str().unwrap(), rel_type);
|
||||||
|
assert_eq!(deser_relation.get("event_id").unwrap().as_str().unwrap(), event_id);
|
||||||
|
assert_eq!(deser_relation.get("key").unwrap().as_str().unwrap(), key);
|
||||||
|
}
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use js_int::uint;
|
use js_int::uint;
|
||||||
use ruma_common::{serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId};
|
use ruma_common::{
|
||||||
use ruma_events::{AnyMessageLikeEvent, MessageLikeEvent};
|
serde::{CanBeEmpty, Raw},
|
||||||
use serde_json::{from_value as from_json_value, json};
|
MilliSecondsSinceUnixEpoch, VoipVersionId,
|
||||||
|
};
|
||||||
|
use ruma_events::{
|
||||||
|
secret_storage::key::{SecretStorageEncryptionAlgorithm, SecretStorageV1AesHmacSha2Properties},
|
||||||
|
AnyGlobalAccountDataEventContent, AnyMessageLikeEvent, AnyMessageLikeEventContent,
|
||||||
|
MessageLikeEvent, RawExt as _,
|
||||||
|
};
|
||||||
|
use serde_json::{from_value as from_json_value, json, value::to_raw_value as to_raw_json_value};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui() {
|
fn ui() {
|
||||||
@ -46,3 +53,53 @@ fn deserialize_message_event() {
|
|||||||
assert_eq!(content.call_id, "foofoo");
|
assert_eq!(content.call_id, "foofoo");
|
||||||
assert_eq!(content.version, VoipVersionId::V0);
|
assert_eq!(content.version, VoipVersionId::V0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_msgtype_plain_text_deserialization_as_any() {
|
||||||
|
let serialized = json!({
|
||||||
|
"body": "Hello world!",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
});
|
||||||
|
|
||||||
|
let raw_event: Raw<AnyMessageLikeEventContent> =
|
||||||
|
Raw::from_json_string(serialized.to_string()).unwrap();
|
||||||
|
|
||||||
|
let event = raw_event.deserialize_with_type("m.room.message".into()).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(event, AnyMessageLikeEventContent::RoomMessage(content));
|
||||||
|
assert_eq!(content.body(), "Hello world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secret_storage_key_deserialization_as_any() {
|
||||||
|
let serialized = to_raw_json_value(&json!({
|
||||||
|
"name": "my_key",
|
||||||
|
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||||
|
"iv": "YWJjZGVmZ2hpamtsbW5vcA",
|
||||||
|
"mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let raw_event: Raw<AnyGlobalAccountDataEventContent> =
|
||||||
|
Raw::from_json_string(serialized.to_string()).unwrap();
|
||||||
|
|
||||||
|
let event = raw_event.deserialize_with_type("m.secret_storage.key.test".into()).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(event, AnyGlobalAccountDataEventContent::SecretStorageKey(content));
|
||||||
|
|
||||||
|
assert_eq!(content.name.unwrap(), "my_key");
|
||||||
|
assert_eq!(content.key_id, "test");
|
||||||
|
assert_matches!(content.passphrase, None);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
content.algorithm,
|
||||||
|
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
|
||||||
|
iv: Some(iv),
|
||||||
|
mac: Some(mac),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
|
||||||
|
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
mod audio;
|
mod audio;
|
||||||
|
mod beacon;
|
||||||
|
mod beacon_info;
|
||||||
mod call;
|
mod call;
|
||||||
mod encrypted;
|
mod encrypted;
|
||||||
mod enums;
|
mod enums;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use assign::assign;
|
use assign::assign;
|
||||||
use ruma_common::owned_event_id;
|
use ruma_common::{owned_event_id, serde::Raw};
|
||||||
use ruma_events::{
|
use ruma_events::{
|
||||||
relation::{CustomRelation, InReplyTo, Replacement, Thread},
|
relation::{CustomRelation, InReplyTo, Replacement, Thread},
|
||||||
room::message::{MessageType, Relation, RoomMessageEventContent},
|
room::message::{MessageType, Relation, RoomMessageEventContent},
|
||||||
@ -52,6 +52,22 @@ fn reply_serialize() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_serialization_roundtrip() {
|
||||||
|
let body = "This is a reply";
|
||||||
|
let mut content = RoomMessageEventContent::text_plain(body);
|
||||||
|
let reply = InReplyTo::new(owned_event_id!("$1598361704261elfgc"));
|
||||||
|
content.relates_to = Some(Relation::Reply { in_reply_to: reply.clone() });
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg));
|
||||||
|
assert_eq!(deser_msg.body, body);
|
||||||
|
assert_matches!(content.relates_to.unwrap(), Relation::Reply { in_reply_to: deser_reply });
|
||||||
|
assert_eq!(deser_reply.event_id, reply.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn replacement_serialize() {
|
fn replacement_serialize() {
|
||||||
let content = assign!(
|
let content = assign!(
|
||||||
@ -111,6 +127,28 @@ fn replacement_deserialize() {
|
|||||||
assert_eq!(text.body, "Hello! My name is bar");
|
assert_eq!(text.body, "Hello! My name is bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacement_serialization_roundtrip() {
|
||||||
|
let body = "<text msg>";
|
||||||
|
let mut content = RoomMessageEventContent::text_plain(body);
|
||||||
|
let new_body = "This is the new content.";
|
||||||
|
let replacement = Replacement::new(
|
||||||
|
owned_event_id!("$1598361704261elfgc"),
|
||||||
|
RoomMessageEventContent::text_plain(new_body).into(),
|
||||||
|
);
|
||||||
|
content.relates_to = Some(Relation::Replacement(replacement.clone()));
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg));
|
||||||
|
assert_eq!(deser_msg.body, body);
|
||||||
|
assert_matches!(content.relates_to.unwrap(), Relation::Replacement(deser_replacement));
|
||||||
|
assert_eq!(deser_replacement.event_id, replacement.event_id);
|
||||||
|
assert_matches!(deser_replacement.new_content.msgtype, MessageType::Text(deser_new_msg));
|
||||||
|
assert_eq!(deser_new_msg.body, new_body);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn thread_plain_serialize() {
|
fn thread_plain_serialize() {
|
||||||
let content = assign!(
|
let content = assign!(
|
||||||
@ -250,6 +288,25 @@ fn thread_unstable_deserialize() {
|
|||||||
assert!(!thread.is_falling_back);
|
assert!(!thread.is_falling_back);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn thread_serialization_roundtrip() {
|
||||||
|
let body = "<text msg>";
|
||||||
|
let mut content = RoomMessageEventContent::text_plain(body);
|
||||||
|
let thread =
|
||||||
|
Thread::plain(owned_event_id!("$1598361704261elfgc"), owned_event_id!("$latesteventid"));
|
||||||
|
content.relates_to = Some(Relation::Thread(thread.clone()));
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg));
|
||||||
|
assert_eq!(deser_msg.body, body);
|
||||||
|
assert_matches!(content.relates_to.unwrap(), Relation::Thread(deser_thread));
|
||||||
|
assert_eq!(deser_thread.event_id, thread.event_id);
|
||||||
|
assert_eq!(deser_thread.in_reply_to.unwrap().event_id, thread.in_reply_to.unwrap().event_id);
|
||||||
|
assert_eq!(deser_thread.is_falling_back, thread.is_falling_back);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_deserialize() {
|
fn custom_deserialize() {
|
||||||
let relation_json = json!({
|
let relation_json = json!({
|
||||||
@ -300,3 +357,33 @@ fn custom_serialize() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_serialization_roundtrip() {
|
||||||
|
let rel_type = "io.ruma.unknown";
|
||||||
|
let event_id = "$related_event";
|
||||||
|
let key = "value";
|
||||||
|
let json_relation = json!({
|
||||||
|
"rel_type": rel_type,
|
||||||
|
"event_id": event_id,
|
||||||
|
"key": key,
|
||||||
|
});
|
||||||
|
let relation = from_json_value::<CustomRelation>(json_relation).unwrap();
|
||||||
|
|
||||||
|
let body = "<text msg>";
|
||||||
|
let mut content = RoomMessageEventContent::text_plain(body);
|
||||||
|
content.relates_to = Some(Relation::_Custom(relation));
|
||||||
|
|
||||||
|
let json_content = Raw::new(&content).unwrap();
|
||||||
|
let deser_content = json_content.deserialize().unwrap();
|
||||||
|
|
||||||
|
assert_matches!(deser_content.msgtype, MessageType::Text(deser_msg));
|
||||||
|
assert_eq!(deser_msg.body, body);
|
||||||
|
let deser_relates_to = deser_content.relates_to.unwrap();
|
||||||
|
assert_matches!(&deser_relates_to, Relation::_Custom(_));
|
||||||
|
assert_eq!(deser_relates_to.rel_type().unwrap().as_str(), rel_type);
|
||||||
|
let deser_relation = deser_relates_to.data();
|
||||||
|
assert_eq!(deser_relation.get("rel_type").unwrap().as_str().unwrap(), rel_type);
|
||||||
|
assert_eq!(deser_relation.get("event_id").unwrap().as_str().unwrap(), event_id);
|
||||||
|
assert_eq!(deser_relation.get("key").unwrap().as_str().unwrap(), key);
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ use ruma_events::{
|
|||||||
},
|
},
|
||||||
EncryptedFileInit, JsonWebKeyInit, MediaSource,
|
EncryptedFileInit, JsonWebKeyInit, MediaSource,
|
||||||
},
|
},
|
||||||
AnySyncTimelineEvent, Mentions, MessageLikeUnsigned,
|
AnySyncTimelineEvent, EventContent, Mentions, MessageLikeUnsigned, RawExt,
|
||||||
};
|
};
|
||||||
use serde_json::{
|
use serde_json::{
|
||||||
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
|
||||||
@ -484,6 +484,49 @@ fn reply_thread_fallback() {
|
|||||||
assert!(thread_info.is_falling_back);
|
assert!(thread_info.is_falling_back);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_thread_serialization_roundtrip() {
|
||||||
|
let thread_root = OriginalRoomMessageEvent {
|
||||||
|
content: RoomMessageEventContent::text_plain("Thread root"),
|
||||||
|
event_id: owned_event_id!("$thread_root"),
|
||||||
|
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
|
||||||
|
room_id: owned_room_id!("!testroomid:example.org"),
|
||||||
|
sender: owned_user_id!("@user:example.org"),
|
||||||
|
unsigned: MessageLikeUnsigned::default(),
|
||||||
|
};
|
||||||
|
let threaded_message = OriginalRoomMessageEvent {
|
||||||
|
content: RoomMessageEventContent::text_plain("Threaded message").make_for_thread(
|
||||||
|
&thread_root,
|
||||||
|
ReplyWithinThread::No,
|
||||||
|
AddMentions::No,
|
||||||
|
),
|
||||||
|
event_id: owned_event_id!("$threaded_message"),
|
||||||
|
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
|
||||||
|
room_id: owned_room_id!("!testroomid:example.org"),
|
||||||
|
sender: owned_user_id!("@user:example.org"),
|
||||||
|
unsigned: MessageLikeUnsigned::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let reply_as_thread_fallback = RoomMessageEventContent::text_plain(
|
||||||
|
"Reply from a thread client",
|
||||||
|
)
|
||||||
|
.make_reply_to(&threaded_message, ForwardThread::Yes, AddMentions::No);
|
||||||
|
|
||||||
|
let as_raw = Raw::new(&reply_as_thread_fallback).unwrap();
|
||||||
|
|
||||||
|
let reply_as_thread_fallback =
|
||||||
|
as_raw.deserialize_with_type(reply_as_thread_fallback.event_type()).unwrap();
|
||||||
|
|
||||||
|
let relation = reply_as_thread_fallback.relates_to.unwrap();
|
||||||
|
assert_matches!(relation, Relation::Thread(thread_info));
|
||||||
|
assert_eq!(
|
||||||
|
thread_info.in_reply_to.map(|in_reply_to| in_reply_to.event_id),
|
||||||
|
Some(threaded_message.event_id)
|
||||||
|
);
|
||||||
|
assert_eq!(thread_info.event_id, thread_root.event_id);
|
||||||
|
assert!(thread_info.is_falling_back);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reply_add_mentions() {
|
fn reply_add_mentions() {
|
||||||
let user = owned_user_id!("@user:example.org");
|
let user = owned_user_id!("@user:example.org");
|
||||||
|
@ -33,7 +33,7 @@ pub mod v1 {
|
|||||||
pub query_type: String,
|
pub query_type: String,
|
||||||
|
|
||||||
/// The query parameters.
|
/// The query parameters.
|
||||||
#[ruma_api(query_map)]
|
#[ruma_api(query_all)]
|
||||||
pub params: BTreeMap<String, String>,
|
pub params: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Breaking Changes:
|
||||||
|
|
||||||
|
- `MatrixElement::Div` is now a newtype variant.
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
- Add support for mathematical messages, according to MSC2191 / Matrix 1.11
|
||||||
|
|
||||||
# 0.2.0
|
# 0.2.0
|
||||||
|
|
||||||
Breaking Changes:
|
Breaking Changes:
|
||||||
|
@ -152,7 +152,7 @@ pub enum MatrixElement {
|
|||||||
/// [`<div>`], a content division element.
|
/// [`<div>`], a content division element.
|
||||||
///
|
///
|
||||||
/// [`<div>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
|
/// [`<div>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
|
||||||
Div,
|
Div(DivData),
|
||||||
|
|
||||||
/// [`<table>`], a table element.
|
/// [`<table>`], a table element.
|
||||||
///
|
///
|
||||||
@ -268,7 +268,10 @@ impl MatrixElement {
|
|||||||
}
|
}
|
||||||
b"hr" => (Self::Hr, attrs.clone()),
|
b"hr" => (Self::Hr, attrs.clone()),
|
||||||
b"br" => (Self::Br, attrs.clone()),
|
b"br" => (Self::Br, attrs.clone()),
|
||||||
b"div" => (Self::Div, attrs.clone()),
|
b"div" => {
|
||||||
|
let (data, attrs) = DivData::parse(attrs);
|
||||||
|
(Self::Div(data), attrs)
|
||||||
|
}
|
||||||
b"table" => (Self::Table, attrs.clone()),
|
b"table" => (Self::Table, attrs.clone()),
|
||||||
b"thead" => (Self::Thead, attrs.clone()),
|
b"thead" => (Self::Thead, attrs.clone()),
|
||||||
b"tbody" => (Self::Tbody, attrs.clone()),
|
b"tbody" => (Self::Tbody, attrs.clone()),
|
||||||
@ -599,12 +602,23 @@ pub struct SpanData {
|
|||||||
///
|
///
|
||||||
/// [spoiler message]: https://spec.matrix.org/latest/client-server-api/#spoiler-messages
|
/// [spoiler message]: https://spec.matrix.org/latest/client-server-api/#spoiler-messages
|
||||||
pub spoiler: Option<StrTendril>,
|
pub spoiler: Option<StrTendril>,
|
||||||
|
|
||||||
|
/// `data-mx-maths`, an inline Matrix [mathematical message].
|
||||||
|
///
|
||||||
|
/// The value is the mathematical notation in [LaTeX] format.
|
||||||
|
///
|
||||||
|
/// If this attribute is present, the content of the span is the fallback representation of the
|
||||||
|
/// mathematical notation.
|
||||||
|
///
|
||||||
|
/// [mathematical message]: https://spec.matrix.org/latest/client-server-api/#mathematical-messages
|
||||||
|
/// [LaTeX]: https://www.latex-project.org/
|
||||||
|
pub maths: Option<StrTendril>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpanData {
|
impl SpanData {
|
||||||
/// Construct an empty `SpanData`.
|
/// Construct an empty `SpanData`.
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self { bg_color: None, color: None, spoiler: None }
|
Self { bg_color: None, color: None, spoiler: None, maths: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the given attributes to construct a new `SpanData`.
|
/// Parse the given attributes to construct a new `SpanData`.
|
||||||
@ -629,6 +643,9 @@ impl SpanData {
|
|||||||
b"data-mx-spoiler" => {
|
b"data-mx-spoiler" => {
|
||||||
data.spoiler = Some(attr.value.clone());
|
data.spoiler = Some(attr.value.clone());
|
||||||
}
|
}
|
||||||
|
b"data-mx-maths" => {
|
||||||
|
data.maths = Some(attr.value.clone());
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
remaining_attrs.insert(attr.clone());
|
remaining_attrs.insert(attr.clone());
|
||||||
}
|
}
|
||||||
@ -722,3 +739,53 @@ impl ImageData {
|
|||||||
(data, remaining_attrs)
|
(data, remaining_attrs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The supported data of a `<div>` HTML element.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct DivData {
|
||||||
|
/// `data-mx-maths`, a Matrix [mathematical message] block.
|
||||||
|
///
|
||||||
|
/// The value is the mathematical notation in [LaTeX] format.
|
||||||
|
///
|
||||||
|
/// If this attribute is present, the content of the div is the fallback representation of the
|
||||||
|
/// mathematical notation.
|
||||||
|
///
|
||||||
|
/// [mathematical message]: https://spec.matrix.org/latest/client-server-api/#mathematical-messages
|
||||||
|
/// [LaTeX]: https://www.latex-project.org/
|
||||||
|
pub maths: Option<StrTendril>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivData {
|
||||||
|
/// Construct an empty `DivData`.
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { maths: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the given attributes to construct a new `SpanData`.
|
||||||
|
///
|
||||||
|
/// Returns a tuple containing the constructed data and the remaining unsupported attributes.
|
||||||
|
#[allow(clippy::mutable_key_type)]
|
||||||
|
fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
|
||||||
|
let mut data = Self::new();
|
||||||
|
let mut remaining_attrs = BTreeSet::new();
|
||||||
|
|
||||||
|
for attr in attrs {
|
||||||
|
if attr.name.ns != ns!() {
|
||||||
|
remaining_attrs.insert(attr.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match attr.name.local.as_bytes() {
|
||||||
|
b"data-mx-maths" => {
|
||||||
|
data.maths = Some(attr.value.clone());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
remaining_attrs.insert(attr.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(data, remaining_attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -28,14 +28,16 @@ static ALLOWED_ATTRIBUTES_STRICT: Map<&str, &Set<&str>> = phf_map! {
|
|||||||
"img" => &ALLOWED_ATTRIBUTES_IMG_STRICT,
|
"img" => &ALLOWED_ATTRIBUTES_IMG_STRICT,
|
||||||
"ol" => &ALLOWED_ATTRIBUTES_OL_STRICT,
|
"ol" => &ALLOWED_ATTRIBUTES_OL_STRICT,
|
||||||
"code" => &ALLOWED_ATTRIBUTES_CODE_STRICT,
|
"code" => &ALLOWED_ATTRIBUTES_CODE_STRICT,
|
||||||
|
"div" => &ALLOWED_ATTRIBUTES_DIV_STRICT,
|
||||||
};
|
};
|
||||||
static ALLOWED_ATTRIBUTES_SPAN_STRICT: Set<&str> =
|
static ALLOWED_ATTRIBUTES_SPAN_STRICT: Set<&str> =
|
||||||
phf_set! { "data-mx-bg-color", "data-mx-color", "data-mx-spoiler" };
|
phf_set! { "data-mx-bg-color", "data-mx-color", "data-mx-spoiler", "data-mx-maths" };
|
||||||
static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" };
|
static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" };
|
||||||
static ALLOWED_ATTRIBUTES_IMG_STRICT: Set<&str> =
|
static ALLOWED_ATTRIBUTES_IMG_STRICT: Set<&str> =
|
||||||
phf_set! { "width", "height", "alt", "title", "src" };
|
phf_set! { "width", "height", "alt", "title", "src" };
|
||||||
static ALLOWED_ATTRIBUTES_OL_STRICT: Set<&str> = phf_set! { "start" };
|
static ALLOWED_ATTRIBUTES_OL_STRICT: Set<&str> = phf_set! { "start" };
|
||||||
static ALLOWED_ATTRIBUTES_CODE_STRICT: Set<&str> = phf_set! { "class" };
|
static ALLOWED_ATTRIBUTES_CODE_STRICT: Set<&str> = phf_set! { "class" };
|
||||||
|
static ALLOWED_ATTRIBUTES_DIV_STRICT: Set<&str> = phf_set! { "data-mx-maths" };
|
||||||
|
|
||||||
/// Attributes that were previously allowed on HTML elements according to the Matrix specification,
|
/// Attributes that were previously allowed on HTML elements according to the Matrix specification,
|
||||||
/// with their replacement.
|
/// with their replacement.
|
||||||
|
@ -26,7 +26,8 @@ fn elements() {
|
|||||||
// `<div>` element.
|
// `<div>` element.
|
||||||
let div_node = html_children.next().unwrap();
|
let div_node = html_children.next().unwrap();
|
||||||
let div_element = div_node.as_element().unwrap().to_matrix();
|
let div_element = div_node.as_element().unwrap().to_matrix();
|
||||||
assert_matches!(div_element.element, MatrixElement::Div);
|
assert_matches!(div_element.element, MatrixElement::Div(div));
|
||||||
|
assert_eq!(div.maths, None);
|
||||||
// The `class` attribute is not supported.
|
// The `class` attribute is not supported.
|
||||||
assert_eq!(div_element.attrs.len(), 1);
|
assert_eq!(div_element.attrs.len(), 1);
|
||||||
|
|
||||||
|
@ -5,10 +5,8 @@
|
|||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
- Allow underscores (`_`) when validating MXC URIs.
|
- Allow underscores (`_`) when validating MXC URIs.
|
||||||
- They have always been allowed in [the spec][mxc validation spec]
|
- They have always been allowed in the spec in order to support URL-safe
|
||||||
in order to support URL-safe base64-encoded media IDs.
|
base64-encoded media IDs.
|
||||||
|
|
||||||
[mxc validation spec]: https://spec.matrix.org/v1.9/client-server-api/#security-considerations-5
|
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ pub enum MxcUriError {
|
|||||||
/// Media identifier malformed due to invalid characters detected.
|
/// Media identifier malformed due to invalid characters detected.
|
||||||
///
|
///
|
||||||
/// Valid characters are (in regex notation) `[A-Za-z0-9_-]+`.
|
/// Valid characters are (in regex notation) `[A-Za-z0-9_-]+`.
|
||||||
/// See [here](https://spec.matrix.org/v1.10/client-server-api/#security-considerations-5) for more details.
|
/// See [here](https://spec.matrix.org/v1.11/client-server-api/#security-considerations-5) for more details.
|
||||||
#[error("Media Identifier malformed, invalid characters")]
|
#[error("Media Identifier malformed, invalid characters")]
|
||||||
MediaIdMalformed,
|
MediaIdMalformed,
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ pub fn validate(uri: &str) -> Result<NonZeroU8, MxcUriError> {
|
|||||||
|
|
||||||
let server_name = &uri[..index];
|
let server_name = &uri[..index];
|
||||||
let media_id = &uri[index + 1..];
|
let media_id = &uri[index + 1..];
|
||||||
// See: https://spec.matrix.org/v1.10/client-server-api/#security-considerations-5
|
// See: https://spec.matrix.org/v1.11/client-server-api/#security-considerations-5
|
||||||
let media_id_is_valid = media_id
|
let media_id_is_valid = media_id
|
||||||
.bytes()
|
.bytes()
|
||||||
.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_' ));
|
.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_' ));
|
||||||
|
@ -10,7 +10,7 @@ mod kw {
|
|||||||
syn::custom_keyword!(raw_body);
|
syn::custom_keyword!(raw_body);
|
||||||
syn::custom_keyword!(path);
|
syn::custom_keyword!(path);
|
||||||
syn::custom_keyword!(query);
|
syn::custom_keyword!(query);
|
||||||
syn::custom_keyword!(query_map);
|
syn::custom_keyword!(query_all);
|
||||||
syn::custom_keyword!(header);
|
syn::custom_keyword!(header);
|
||||||
syn::custom_keyword!(error);
|
syn::custom_keyword!(error);
|
||||||
syn::custom_keyword!(manual_body_serde);
|
syn::custom_keyword!(manual_body_serde);
|
||||||
@ -22,7 +22,7 @@ pub enum RequestMeta {
|
|||||||
RawBody,
|
RawBody,
|
||||||
Path,
|
Path,
|
||||||
Query,
|
Query,
|
||||||
QueryMap,
|
QueryAll,
|
||||||
Header(Ident),
|
Header(Ident),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,9 +41,9 @@ impl Parse for RequestMeta {
|
|||||||
} else if lookahead.peek(kw::query) {
|
} else if lookahead.peek(kw::query) {
|
||||||
let _: kw::query = input.parse()?;
|
let _: kw::query = input.parse()?;
|
||||||
Ok(Self::Query)
|
Ok(Self::Query)
|
||||||
} else if lookahead.peek(kw::query_map) {
|
} else if lookahead.peek(kw::query_all) {
|
||||||
let _: kw::query_map = input.parse()?;
|
let _: kw::query_all = input.parse()?;
|
||||||
Ok(Self::QueryMap)
|
Ok(Self::QueryAll)
|
||||||
} else if lookahead.peek(kw::header) {
|
} else if lookahead.peek(kw::header) {
|
||||||
let _: kw::header = input.parse()?;
|
let _: kw::header = input.parse()?;
|
||||||
let _: Token![=] = input.parse()?;
|
let _: Token![=] = input.parse()?;
|
||||||
|
@ -137,8 +137,8 @@ impl Request {
|
|||||||
self.fields.iter().find_map(RequestField::as_raw_body_field)
|
self.fields.iter().find_map(RequestField::as_raw_body_field)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_map_field(&self) -> Option<&Field> {
|
fn query_all_field(&self) -> Option<&Field> {
|
||||||
self.fields.iter().find_map(RequestField::as_query_map_field)
|
self.fields.iter().find_map(RequestField::as_query_all_field)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_all(&self, ruma_common: &TokenStream) -> TokenStream {
|
fn expand_all(&self, ruma_common: &TokenStream) -> TokenStream {
|
||||||
@ -161,7 +161,7 @@ impl Request {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let request_query_def = if let Some(f) = self.query_map_field() {
|
let request_query_def = if let Some(f) = self.query_all_field() {
|
||||||
let field = Field { ident: None, colon_token: None, ..f.clone() };
|
let field = Field { ident: None, colon_token: None, ..f.clone() };
|
||||||
let field = PrivateField(&field);
|
let field = PrivateField(&field);
|
||||||
Some(quote! { (#field); })
|
Some(quote! { (#field); })
|
||||||
@ -220,15 +220,15 @@ impl Request {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let query_map_fields =
|
let query_all_fields =
|
||||||
self.fields.iter().filter(|f| matches!(&f.kind, RequestFieldKind::QueryMap));
|
self.fields.iter().filter(|f| matches!(&f.kind, RequestFieldKind::QueryAll));
|
||||||
let has_query_map_field = match query_map_fields.count() {
|
let has_query_all_field = match query_all_fields.count() {
|
||||||
0 => false,
|
0 => false,
|
||||||
1 => true,
|
1 => true,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
&self.ident,
|
&self.ident,
|
||||||
"Can't have more than one query_map field",
|
"Can't have more than one query_all field",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -244,10 +244,10 @@ impl Request {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_query_map_field && has_query_fields {
|
if has_query_all_field && has_query_fields {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
&self.ident,
|
&self.ident,
|
||||||
"Can't have both a query map field and regular query fields",
|
"Can't have both a query_all field and regular query fields",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,8 +307,8 @@ pub(super) enum RequestFieldKind {
|
|||||||
/// Data that appears in the query string.
|
/// Data that appears in the query string.
|
||||||
Query,
|
Query,
|
||||||
|
|
||||||
/// Data that appears in the query string as dynamic key-value pairs.
|
/// Data that represents all the query string as a single type.
|
||||||
QueryMap,
|
QueryAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestField {
|
impl RequestField {
|
||||||
@ -319,7 +319,7 @@ impl RequestField {
|
|||||||
Some(RequestMeta::RawBody) => RequestFieldKind::RawBody,
|
Some(RequestMeta::RawBody) => RequestFieldKind::RawBody,
|
||||||
Some(RequestMeta::Path) => RequestFieldKind::Path,
|
Some(RequestMeta::Path) => RequestFieldKind::Path,
|
||||||
Some(RequestMeta::Query) => RequestFieldKind::Query,
|
Some(RequestMeta::Query) => RequestFieldKind::Query,
|
||||||
Some(RequestMeta::QueryMap) => RequestFieldKind::QueryMap,
|
Some(RequestMeta::QueryAll) => RequestFieldKind::QueryAll,
|
||||||
Some(RequestMeta::Header(header)) => RequestFieldKind::Header(header),
|
Some(RequestMeta::Header(header)) => RequestFieldKind::Header(header),
|
||||||
None => RequestFieldKind::Body,
|
None => RequestFieldKind::Body,
|
||||||
};
|
};
|
||||||
@ -359,10 +359,10 @@ impl RequestField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the contained field if this request field is a query map kind.
|
/// Return the contained field if this request field is a query all kind.
|
||||||
pub fn as_query_map_field(&self) -> Option<&Field> {
|
pub fn as_query_all_field(&self) -> Option<&Field> {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
RequestFieldKind::QueryMap => Some(&self.inner),
|
RequestFieldKind::QueryAll => Some(&self.inner),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ impl Request {
|
|||||||
(TokenStream::new(), TokenStream::new())
|
(TokenStream::new(), TokenStream::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
let (parse_query, query_vars) = if let Some(field) = self.query_map_field() {
|
let (parse_query, query_vars) = if let Some(field) = self.query_all_field() {
|
||||||
let cfg_attrs =
|
let cfg_attrs =
|
||||||
field.attrs.iter().filter(|a| a.path().is_ident("cfg")).collect::<Vec<_>>();
|
field.attrs.iter().filter(|a| a.path().is_ident("cfg")).collect::<Vec<_>>();
|
||||||
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
|
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
|
||||||
|
@ -15,29 +15,11 @@ impl Request {
|
|||||||
let path_fields =
|
let path_fields =
|
||||||
self.path_fields().map(|f| f.ident.as_ref().expect("path fields have a name"));
|
self.path_fields().map(|f| f.ident.as_ref().expect("path fields have a name"));
|
||||||
|
|
||||||
let request_query_string = if let Some(field) = self.query_map_field() {
|
let request_query_string = if let Some(field) = self.query_all_field() {
|
||||||
let field_name = field.ident.as_ref().expect("expected field to have identifier");
|
let field_name = field.ident.as_ref().expect("expected field to have identifier");
|
||||||
|
|
||||||
quote! {{
|
quote! {{
|
||||||
// This function exists so that the compiler will throw an error when the type of
|
|
||||||
// the field with the query_map attribute doesn't implement
|
|
||||||
// `IntoIterator<Item = (String, String)>`.
|
|
||||||
//
|
|
||||||
// This is necessary because the `serde_html_form::to_string` call will result in a
|
|
||||||
// runtime error when the type cannot be encoded as a list key-value pairs
|
|
||||||
// (?key1=value1&key2=value2).
|
|
||||||
//
|
|
||||||
// By asserting that it implements the iterator trait, we can ensure that it won't
|
|
||||||
// fail.
|
|
||||||
fn assert_trait_impl<T>(_: &T)
|
|
||||||
where
|
|
||||||
T: ::std::iter::IntoIterator<
|
|
||||||
Item = (::std::string::String, ::std::string::String),
|
|
||||||
>,
|
|
||||||
{}
|
|
||||||
|
|
||||||
let request_query = RequestQuery(self.#field_name);
|
let request_query = RequestQuery(self.#field_name);
|
||||||
assert_trait_impl(&request_query.0);
|
|
||||||
|
|
||||||
&#serde_html_form::to_string(request_query)?
|
&#serde_html_form::to_string(request_query)?
|
||||||
}}
|
}}
|
||||||
|
@ -170,7 +170,17 @@ fn expand_deserialize_impl(
|
|||||||
};
|
};
|
||||||
let self_variant = variant.ctor(quote! { Self });
|
let self_variant = variant.ctor(quote! { Self });
|
||||||
let content = event.to_event_path(kind, var);
|
let content = event.to_event_path(kind, var);
|
||||||
let ev_types = event.aliases.iter().chain([&event.ev_type]);
|
let ev_types = event.aliases.iter().chain([&event.ev_type]).map(|ev_type| {
|
||||||
|
if event.has_type_fragment() {
|
||||||
|
let ev_type = ev_type.value();
|
||||||
|
let prefix = ev_type
|
||||||
|
.strip_suffix('*')
|
||||||
|
.expect("event type with type fragment must end with *");
|
||||||
|
quote! { t if t.starts_with(#prefix) }
|
||||||
|
} else {
|
||||||
|
quote! { #ev_type }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#variant_attrs #(#ev_types)|* => {
|
#variant_attrs #(#ev_types)|* => {
|
||||||
@ -328,6 +338,55 @@ fn expand_content_enum(
|
|||||||
let serialize_custom_event_error_path =
|
let serialize_custom_event_error_path =
|
||||||
quote! { #ruma_events::serialize_custom_event_error }.to_string();
|
quote! { #ruma_events::serialize_custom_event_error }.to_string();
|
||||||
|
|
||||||
|
// Generate an `EventContentFromType` implementation.
|
||||||
|
let serde_json = quote! { #ruma_events::exports::serde_json };
|
||||||
|
let event_type_match_arms: TokenStream = events
|
||||||
|
.iter()
|
||||||
|
.map(|event| {
|
||||||
|
let variant = event.to_variant()?;
|
||||||
|
let variant_attrs = {
|
||||||
|
let attrs = &variant.attrs;
|
||||||
|
quote! { #(#attrs)* }
|
||||||
|
};
|
||||||
|
let self_variant = variant.ctor(quote! { Self });
|
||||||
|
|
||||||
|
let ev_types = event.aliases.iter().chain([&event.ev_type]).map(|ev_type| {
|
||||||
|
if event.has_type_fragment() {
|
||||||
|
let ev_type = ev_type.value();
|
||||||
|
let prefix = ev_type
|
||||||
|
.strip_suffix('*')
|
||||||
|
.expect("event type with type fragment must end with *");
|
||||||
|
quote! { t if t.starts_with(#prefix) }
|
||||||
|
} else {
|
||||||
|
quote! { #ev_type }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let deserialize_content = if event.has_type_fragment() {
|
||||||
|
// If the event has a type fragment, then it implements EventContentFromType itself;
|
||||||
|
// see `generate_event_content_impl` which does that. In this case, forward to its
|
||||||
|
// implementation.
|
||||||
|
let content_type = event.to_event_content_path(kind, None);
|
||||||
|
quote! {
|
||||||
|
#content_type::from_parts(event_type, json)?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The event doesn't have a type fragment, so it *should* implement Deserialize:
|
||||||
|
// use that here.
|
||||||
|
quote! {
|
||||||
|
#serde_json::from_str(json.get())?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#variant_attrs #(#ev_types)|* => {
|
||||||
|
let content = #deserialize_content;
|
||||||
|
Ok(#self_variant(content))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<syn::Result<_>>()?;
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#( #attrs )*
|
#( #attrs )*
|
||||||
#[derive(Clone, Debug, #serde::Serialize)]
|
#[derive(Clone, Debug, #serde::Serialize)]
|
||||||
@ -358,6 +417,23 @@ fn expand_content_enum(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[automatically_derived]
|
||||||
|
impl #ruma_events::EventContentFromType for #ident {
|
||||||
|
fn from_parts(event_type: &str, json: &#serde_json::value::RawValue) -> serde_json::Result<Self> {
|
||||||
|
match event_type {
|
||||||
|
#event_type_match_arms
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
Ok(Self::_Custom {
|
||||||
|
event_type: crate::PrivOwnedStr(
|
||||||
|
::std::convert::From::from(event_type.to_owned())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
impl #ruma_events::#sub_trait_name for #ident {
|
impl #ruma_events::#sub_trait_name for #ident {
|
||||||
#state_event_content_impl
|
#state_event_content_impl
|
||||||
|
@ -4,6 +4,19 @@ Breaking changes:
|
|||||||
|
|
||||||
- The `XMatrix::new` method now takes `OwnedServerName` instead of `Option<OwnedServerName>`
|
- The `XMatrix::new` method now takes `OwnedServerName` instead of `Option<OwnedServerName>`
|
||||||
for the destination, since servers must always set the destination.
|
for the destination, since servers must always set the destination.
|
||||||
|
- The `sig` field in `XMatrix` has been changed from `String` to `Base64` to more accurately
|
||||||
|
mirror its allowed values in the type system.
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
- When encoding to a header value, `XMatrix` fields are now quoted and escaped correctly.
|
||||||
|
- Use http-auth crate to parse `XMatrix`. Allows to parse the Authorization HTTP
|
||||||
|
header with full compatibility with RFC 7235
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
- Implement `Display`, `FromStr` and conversion to/from `http::HeaderValue` for
|
||||||
|
`XMatrix`
|
||||||
|
|
||||||
# 0.3.0
|
# 0.3.0
|
||||||
|
|
||||||
|
@ -16,9 +16,11 @@ all-features = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
headers = "0.4.0"
|
headers = "0.4.0"
|
||||||
|
http = { workspace = true }
|
||||||
|
http-auth = { version = "0.1.9", default-features = false }
|
||||||
ruma-common = { workspace = true }
|
ruma-common = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
yap = "0.12.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tracing-subscriber = "0.3.16"
|
tracing-subscriber = "0.3.16"
|
||||||
|
@ -1,30 +1,38 @@
|
|||||||
//! Common types for implementing federation authorization.
|
//! Common types for implementing federation authorization.
|
||||||
|
|
||||||
use headers::{authorization::Credentials, HeaderValue};
|
use std::{borrow::Cow, fmt, str::FromStr};
|
||||||
use ruma_common::{OwnedServerName, OwnedServerSigningKeyId};
|
|
||||||
|
use headers::authorization::Credentials;
|
||||||
|
use http::HeaderValue;
|
||||||
|
use http_auth::ChallengeParser;
|
||||||
|
use ruma_common::{
|
||||||
|
serde::{Base64, Base64DecodeError},
|
||||||
|
IdParseError, OwnedServerName, OwnedServerSigningKeyId,
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use yap::{IntoTokens, TokenLocation, Tokens};
|
|
||||||
|
|
||||||
/// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
|
/// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
|
||||||
/// [Matrix Server-Server API][spec]. Includes an implementation of
|
/// [Matrix Server-Server API][spec].
|
||||||
/// [`headers::authorization::Credentials`] for automatically handling the encoding and decoding
|
|
||||||
/// when using a web framework that supports typed headers.
|
|
||||||
///
|
///
|
||||||
/// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
|
/// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
|
||||||
|
#[derive(Clone)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct XMatrix {
|
pub struct XMatrix {
|
||||||
/// The server name of the sending server.
|
/// The server name of the sending server.
|
||||||
pub origin: OwnedServerName,
|
pub origin: OwnedServerName,
|
||||||
/// The server name of the receiving sender. For compatibility with older servers, recipients
|
/// The server name of the receiving sender.
|
||||||
/// should accept requests without this parameter, but MUST always send it. If this property is
|
///
|
||||||
/// included, but the value does not match the receiving server's name, the receiving server
|
/// For compatibility with older servers, recipients should accept requests without this
|
||||||
/// must deny the request with an HTTP status code 401 Unauthorized.
|
/// parameter, but MUST always send it. If this property is included, but the value does
|
||||||
|
/// not match the receiving server's name, the receiving server must deny the request with
|
||||||
|
/// an HTTP status code 401 Unauthorized.
|
||||||
pub destination: Option<OwnedServerName>,
|
pub destination: Option<OwnedServerName>,
|
||||||
/// The ID - including the algorithm name - of the sending server's key that was used to sign
|
/// The ID - including the algorithm name - of the sending server's key that was used to sign
|
||||||
/// the request.
|
/// the request.
|
||||||
pub key: OwnedServerSigningKeyId,
|
pub key: OwnedServerSigningKeyId,
|
||||||
/// The signature of the JSON.
|
/// The signature of the JSON.
|
||||||
pub sig: String,
|
pub sig: Base64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XMatrix {
|
impl XMatrix {
|
||||||
@ -33,227 +41,214 @@ impl XMatrix {
|
|||||||
origin: OwnedServerName,
|
origin: OwnedServerName,
|
||||||
destination: OwnedServerName,
|
destination: OwnedServerName,
|
||||||
key: OwnedServerSigningKeyId,
|
key: OwnedServerSigningKeyId,
|
||||||
sig: String,
|
sig: Base64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { origin, destination: Some(destination), key, sig }
|
Self { origin, destination: Some(destination), key, sig }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_token<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
/// Parse an X-Matrix Authorization header from the given string.
|
||||||
tokens.optional(|t| {
|
pub fn parse(s: impl AsRef<str>) -> Result<Self, XMatrixParseError> {
|
||||||
let token: Vec<u8> = t.take_while(|c| is_tchar(**c)).as_iter().copied().collect();
|
let parser = ChallengeParser::new(s.as_ref());
|
||||||
if !token.is_empty() {
|
let mut xmatrix = None;
|
||||||
Some(token)
|
|
||||||
} else {
|
|
||||||
debug!("Returning early because of empty token at {}", t.location().offset());
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_token_with_colons<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
for challenge in parser {
|
||||||
tokens.optional(|t| {
|
let challenge = challenge?;
|
||||||
let token: Vec<u8> =
|
|
||||||
t.take_while(|c| is_tchar(**c) || **c == b':').as_iter().copied().collect();
|
|
||||||
if !token.is_empty() {
|
|
||||||
Some(token)
|
|
||||||
} else {
|
|
||||||
debug!("Returning early because of empty token at {}", t.location().offset());
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_quoted<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
if challenge.scheme.eq_ignore_ascii_case(XMatrix::SCHEME) {
|
||||||
tokens.optional(|t| {
|
xmatrix = Some(challenge);
|
||||||
if !(t.token(&b'"')) {
|
break;
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let mut buffer = Vec::new();
|
|
||||||
loop {
|
|
||||||
match t.next()? {
|
|
||||||
// quoted pair
|
|
||||||
b'\\' => {
|
|
||||||
let escaped = t.next().filter(|c| {
|
|
||||||
if is_quoted_pair(**c) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
"Encountered an illegal character {} at location {}",
|
|
||||||
**c as char,
|
|
||||||
t.location().offset()
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
buffer.push(*escaped);
|
|
||||||
}
|
|
||||||
// end of quote
|
|
||||||
b'"' => break,
|
|
||||||
// regular character
|
|
||||||
c if is_qdtext(*c) => buffer.push(*c),
|
|
||||||
// Invalid character
|
|
||||||
c => {
|
|
||||||
debug!(
|
|
||||||
"Encountered an illegal character {} at location {}",
|
|
||||||
*c as char,
|
|
||||||
t.location().offset()
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(buffer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_xmatrix_field<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<(String, Vec<u8>)> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !t.token(&b'=') {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = parse_quoted(t).or_else(|| parse_token_with_colons(t))?;
|
let Some(xmatrix) = xmatrix else {
|
||||||
|
return Err(XMatrixParseError::NotFound);
|
||||||
|
};
|
||||||
|
|
||||||
Some((name, value))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_xmatrix<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<XMatrix> {
|
|
||||||
tokens.optional(|t| {
|
|
||||||
if !t.tokens(b"X-Matrix ") {
|
|
||||||
debug!("Failed to parse X-Matrix credentials, didn't start with 'X-Matrix '");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut origin = None;
|
let mut origin = None;
|
||||||
let mut destination = None;
|
let mut destination = None;
|
||||||
let mut key = None;
|
let mut key = None;
|
||||||
let mut sig = None;
|
let mut sig = None;
|
||||||
|
|
||||||
for (name, value) in t.sep_by(|t| parse_xmatrix_field(t), |t| t.token(&b',')).as_iter() {
|
for (name, value) in xmatrix.params {
|
||||||
match name.as_str() {
|
if name.eq_ignore_ascii_case("origin") {
|
||||||
"origin" => {
|
|
||||||
if origin.is_some() {
|
if origin.is_some() {
|
||||||
debug!("Field origin duplicated in X-Matrix Authorization header");
|
return Err(XMatrixParseError::DuplicateParameter("origin".to_owned()));
|
||||||
|
} else {
|
||||||
|
origin = Some(OwnedServerName::try_from(value.to_unescaped())?);
|
||||||
}
|
}
|
||||||
origin = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
} else if name.eq_ignore_ascii_case("destination") {
|
||||||
}
|
|
||||||
"destination" => {
|
|
||||||
if destination.is_some() {
|
if destination.is_some() {
|
||||||
debug!("Field destination duplicated in X-Matrix Authorization header");
|
return Err(XMatrixParseError::DuplicateParameter("destination".to_owned()));
|
||||||
|
} else {
|
||||||
|
destination = Some(OwnedServerName::try_from(value.to_unescaped())?);
|
||||||
}
|
}
|
||||||
destination = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
} else if name.eq_ignore_ascii_case("key") {
|
||||||
}
|
|
||||||
"key" => {
|
|
||||||
if key.is_some() {
|
if key.is_some() {
|
||||||
debug!("Field key duplicated in X-Matrix Authorization header");
|
return Err(XMatrixParseError::DuplicateParameter("key".to_owned()));
|
||||||
|
} else {
|
||||||
|
key = Some(OwnedServerSigningKeyId::try_from(value.to_unescaped())?);
|
||||||
}
|
}
|
||||||
key = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
} else if name.eq_ignore_ascii_case("sig") {
|
||||||
}
|
|
||||||
"sig" => {
|
|
||||||
if sig.is_some() {
|
if sig.is_some() {
|
||||||
debug!("Field sig duplicated in X-Matrix Authorization header");
|
return Err(XMatrixParseError::DuplicateParameter("sig".to_owned()));
|
||||||
}
|
} else {
|
||||||
sig = Some(std::str::from_utf8(&value).ok()?.to_owned());
|
sig = Some(Base64::parse(value.to_unescaped())?);
|
||||||
}
|
|
||||||
name => {
|
|
||||||
debug!("Unknown field {} found in X-Matrix Authorization header", name);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Unknown parameter {name} in X-Matrix Authorization header");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(XMatrix { origin: origin?, destination, key: key?, sig: sig? })
|
Ok(Self {
|
||||||
|
origin: origin
|
||||||
|
.ok_or_else(|| XMatrixParseError::MissingParameter("origin".to_owned()))?,
|
||||||
|
destination,
|
||||||
|
key: key.ok_or_else(|| XMatrixParseError::MissingParameter("key".to_owned()))?,
|
||||||
|
sig: sig.ok_or_else(|| XMatrixParseError::MissingParameter("sig".to_owned()))?,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_alpha(c: u8) -> bool {
|
impl fmt::Debug for XMatrix {
|
||||||
(0x41..=0x5A).contains(&c) || (0x61..=0x7A).contains(&c)
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("XMatrix")
|
||||||
|
.field("origin", &self.origin)
|
||||||
|
.field("destination", &self.destination)
|
||||||
|
.field("key", &self.key)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_digit(c: u8) -> bool {
|
/// Whether the given char is a [token char].
|
||||||
(0x30..=0x39).contains(&c)
|
///
|
||||||
|
/// [token char]: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2
|
||||||
|
fn is_tchar(c: char) -> bool {
|
||||||
|
const TOKEN_CHARS: [char; 15] =
|
||||||
|
['!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~'];
|
||||||
|
c.is_ascii_alphanumeric() || TOKEN_CHARS.contains(&c)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_tchar(c: u8) -> bool {
|
/// If the field value does not contain only token chars, convert it to a [quoted string].
|
||||||
const TOKEN_CHARS: [u8; 15] =
|
///
|
||||||
[b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'.', b'^', b'_', b'`', b'|', b'~'];
|
/// [quoted string]: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4
|
||||||
is_alpha(c) || is_digit(c) || TOKEN_CHARS.contains(&c)
|
fn escape_field_value(value: &str) -> Cow<'_, str> {
|
||||||
|
if !value.is_empty() && value.chars().all(is_tchar) {
|
||||||
|
return Cow::Borrowed(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = value.replace('\\', r#"\\"#).replace('"', r#"\""#);
|
||||||
|
Cow::Owned(format!("\"{value}\""))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_qdtext(c: u8) -> bool {
|
impl fmt::Display for XMatrix {
|
||||||
c == b'\t'
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|| c == b' '
|
let Self { origin, destination, key, sig } = self;
|
||||||
|| c == 0x21
|
|
||||||
|| (0x23..=0x5B).contains(&c)
|
let origin = escape_field_value(origin.as_str());
|
||||||
|| (0x5D..=0x7E).contains(&c)
|
let key = escape_field_value(key.as_str());
|
||||||
|| is_obs_text(c)
|
let sig = sig.encode();
|
||||||
|
let sig = escape_field_value(&sig);
|
||||||
|
|
||||||
|
write!(f, r#"{} "#, Self::SCHEME)?;
|
||||||
|
|
||||||
|
if let Some(destination) = destination {
|
||||||
|
let destination = escape_field_value(destination.as_str());
|
||||||
|
write!(f, r#"destination={destination},"#)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "key={key},origin={origin},sig={sig}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_obs_text(c: u8) -> bool {
|
impl FromStr for XMatrix {
|
||||||
c >= 0x80 // The spec does contain an upper limit of 0xFF here, but that's enforced by the type
|
type Err = XMatrixParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::parse(s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_vchar(c: u8) -> bool {
|
impl TryFrom<&HeaderValue> for XMatrix {
|
||||||
(0x21..=0x7E).contains(&c)
|
type Error = XMatrixParseError;
|
||||||
|
|
||||||
|
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
|
||||||
|
Self::parse(value.to_str()?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_quoted_pair(c: u8) -> bool {
|
impl From<&XMatrix> for HeaderValue {
|
||||||
c == b'\t' || c == b' ' || is_vchar(c) || is_obs_text(c)
|
fn from(value: &XMatrix) -> Self {
|
||||||
|
value.to_string().try_into().expect("header format is static")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credentials for XMatrix {
|
impl Credentials for XMatrix {
|
||||||
const SCHEME: &'static str = "X-Matrix";
|
const SCHEME: &'static str = "X-Matrix";
|
||||||
|
|
||||||
fn decode(value: &HeaderValue) -> Option<Self> {
|
fn decode(value: &HeaderValue) -> Option<Self> {
|
||||||
let value: Vec<u8> = value.as_bytes().to_vec();
|
value.try_into().ok()
|
||||||
parse_xmatrix(&mut value.into_tokens())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode(&self) -> HeaderValue {
|
fn encode(&self) -> HeaderValue {
|
||||||
if let Some(destination) = &self.destination {
|
self.into()
|
||||||
format!(
|
|
||||||
"X-Matrix origin=\"{}\",destination=\"{destination}\",key=\"{}\",sig=\"{}\"",
|
|
||||||
self.origin, self.key, self.sig
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("X-Matrix origin=\"{}\",key=\"{}\",sig=\"{}\"", self.origin, self.key, self.sig)
|
|
||||||
}
|
}
|
||||||
.try_into()
|
}
|
||||||
.expect("header format is static")
|
|
||||||
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use headers::{authorization::Credentials, HeaderValue};
|
use headers::{authorization::Credentials, HeaderValue};
|
||||||
use ruma_common::OwnedServerName;
|
use ruma_common::{serde::Base64, OwnedServerName};
|
||||||
|
|
||||||
use super::XMatrix;
|
use super::XMatrix;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn xmatrix_auth_pre_1_3() {
|
fn xmatrix_auth_pre_1_3() {
|
||||||
let header = HeaderValue::from_static(
|
let header = HeaderValue::from_static(
|
||||||
"X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"ABCDEF...\"",
|
"X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"",
|
||||||
);
|
);
|
||||||
let origin = "origin.hs.example.com".try_into().unwrap();
|
let origin = "origin.hs.example.com".try_into().unwrap();
|
||||||
let key = "ed25519:key1".try_into().unwrap();
|
let key = "ed25519:key1".try_into().unwrap();
|
||||||
let sig = "ABCDEF...".to_owned();
|
let sig = Base64::new(b"test".to_vec());
|
||||||
let credentials: XMatrix = Credentials::decode(&header).unwrap();
|
let credentials = XMatrix::try_from(&header).unwrap();
|
||||||
assert_eq!(credentials.origin, origin);
|
assert_eq!(credentials.origin, origin);
|
||||||
assert_eq!(credentials.destination, None);
|
assert_eq!(credentials.destination, None);
|
||||||
assert_eq!(credentials.key, key);
|
assert_eq!(credentials.key, key);
|
||||||
@ -261,17 +256,20 @@ mod tests {
|
|||||||
|
|
||||||
let credentials = XMatrix { origin, destination: None, key, sig };
|
let credentials = XMatrix { origin, destination: None, key, sig };
|
||||||
|
|
||||||
assert_eq!(credentials.encode(), header);
|
assert_eq!(
|
||||||
|
credentials.encode(),
|
||||||
|
"X-Matrix key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn xmatrix_auth_1_3() {
|
fn xmatrix_auth_1_3() {
|
||||||
let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"ABCDEF...\"");
|
let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"");
|
||||||
let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap();
|
let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap();
|
||||||
let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap();
|
let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap();
|
||||||
let key = "ed25519:key1".try_into().unwrap();
|
let key = "ed25519:key1".try_into().unwrap();
|
||||||
let sig = "ABCDEF...".to_owned();
|
let sig = Base64::new(b"test".to_vec());
|
||||||
let credentials: XMatrix = Credentials::decode(&header).unwrap();
|
let credentials = XMatrix::try_from(&header).unwrap();
|
||||||
assert_eq!(credentials.origin, origin);
|
assert_eq!(credentials.origin, origin);
|
||||||
assert_eq!(credentials.destination, Some(destination.clone()));
|
assert_eq!(credentials.destination, Some(destination.clone()));
|
||||||
assert_eq!(credentials.key, key);
|
assert_eq!(credentials.key, key);
|
||||||
@ -279,6 +277,41 @@ mod tests {
|
|||||||
|
|
||||||
let credentials = XMatrix::new(origin, destination, key, sig);
|
let credentials = XMatrix::new(origin, destination, key, sig);
|
||||||
|
|
||||||
assert_eq!(credentials.encode(), header);
|
assert_eq!(credentials.encode(), "X-Matrix destination=destination.hs.example.com,key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xmatrix_quoting() {
|
||||||
|
let header = HeaderValue::from_static(
|
||||||
|
r#"X-Matrix origin="example.com:1234",key="abc\"def\\:ghi",sig=dGVzdA,"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let origin: OwnedServerName = "example.com:1234".try_into().unwrap();
|
||||||
|
let key = r#"abc"def\:ghi"#.try_into().unwrap();
|
||||||
|
let sig = Base64::new(b"test".to_vec());
|
||||||
|
let credentials = XMatrix::try_from(&header).unwrap();
|
||||||
|
assert_eq!(credentials.origin, origin);
|
||||||
|
assert_eq!(credentials.destination, None);
|
||||||
|
assert_eq!(credentials.key, key);
|
||||||
|
assert_eq!(credentials.sig, sig);
|
||||||
|
|
||||||
|
let credentials = XMatrix { origin, destination: None, key, sig };
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
credentials.encode(),
|
||||||
|
r#"X-Matrix key="abc\"def\\:ghi",origin="example.com:1234",sig=dGVzdA"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xmatrix_auth_1_3_with_extra_spaces() {
|
||||||
|
let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\" , destination=\"destination.hs.example.com\",key=\"ed25519:key1\", sig=\"dGVzdA\"");
|
||||||
|
let credentials = XMatrix::try_from(&header).unwrap();
|
||||||
|
let sig = Base64::new(b"test".to_vec());
|
||||||
|
|
||||||
|
assert_eq!(credentials.origin, "origin.hs.example.com");
|
||||||
|
assert_eq!(credentials.destination.unwrap(), "destination.hs.example.com");
|
||||||
|
assert_eq!(credentials.key, "ed25519:key1");
|
||||||
|
assert_eq!(credentials.sig, sig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,7 +226,6 @@ unstable-msc2448 = [
|
|||||||
]
|
]
|
||||||
unstable-msc2654 = ["ruma-client-api?/unstable-msc2654"]
|
unstable-msc2654 = ["ruma-client-api?/unstable-msc2654"]
|
||||||
unstable-msc2666 = ["ruma-client-api?/unstable-msc2666"]
|
unstable-msc2666 = ["ruma-client-api?/unstable-msc2666"]
|
||||||
unstable-msc2705 = ["ruma-client-api?/unstable-msc2705"]
|
|
||||||
unstable-msc2747 = ["ruma-events?/unstable-msc2747"]
|
unstable-msc2747 = ["ruma-events?/unstable-msc2747"]
|
||||||
unstable-msc2867 = ["ruma-events?/unstable-msc2867"]
|
unstable-msc2867 = ["ruma-events?/unstable-msc2867"]
|
||||||
unstable-msc2870 = ["ruma-common/unstable-msc2870"]
|
unstable-msc2870 = ["ruma-common/unstable-msc2870"]
|
||||||
@ -242,10 +241,10 @@ unstable-msc3245 = ["ruma-events?/unstable-msc3245"]
|
|||||||
unstable-msc3245-v1-compat = ["ruma-events?/unstable-msc3245-v1-compat"]
|
unstable-msc3245-v1-compat = ["ruma-events?/unstable-msc3245-v1-compat"]
|
||||||
unstable-msc3246 = ["ruma-events?/unstable-msc3246"]
|
unstable-msc3246 = ["ruma-events?/unstable-msc3246"]
|
||||||
unstable-msc3266 = ["ruma-client-api?/unstable-msc3266"]
|
unstable-msc3266 = ["ruma-client-api?/unstable-msc3266"]
|
||||||
unstable-msc3291 = ["ruma-events?/unstable-msc3291"]
|
|
||||||
unstable-msc3381 = ["ruma-events?/unstable-msc3381"]
|
unstable-msc3381 = ["ruma-events?/unstable-msc3381"]
|
||||||
unstable-msc3401 = ["ruma-events?/unstable-msc3401"]
|
unstable-msc3401 = ["ruma-events?/unstable-msc3401"]
|
||||||
unstable-msc3488 = ["ruma-client-api?/unstable-msc3488", "ruma-events?/unstable-msc3488"]
|
unstable-msc3488 = ["ruma-client-api?/unstable-msc3488", "ruma-events?/unstable-msc3488"]
|
||||||
|
unstable-msc3489 = ["ruma-events?/unstable-msc3489"]
|
||||||
unstable-msc3551 = ["ruma-events?/unstable-msc3551"]
|
unstable-msc3551 = ["ruma-events?/unstable-msc3551"]
|
||||||
unstable-msc3552 = ["ruma-events?/unstable-msc3552"]
|
unstable-msc3552 = ["ruma-events?/unstable-msc3552"]
|
||||||
unstable-msc3553 = ["ruma-events?/unstable-msc3553"]
|
unstable-msc3553 = ["ruma-events?/unstable-msc3553"]
|
||||||
@ -255,7 +254,6 @@ unstable-msc3618 = ["ruma-federation-api?/unstable-msc3618"]
|
|||||||
unstable-msc3723 = ["ruma-federation-api?/unstable-msc3723"]
|
unstable-msc3723 = ["ruma-federation-api?/unstable-msc3723"]
|
||||||
unstable-msc3814 = ["ruma-client-api?/unstable-msc3814"]
|
unstable-msc3814 = ["ruma-client-api?/unstable-msc3814"]
|
||||||
unstable-msc3843 = ["ruma-client-api?/unstable-msc3843", "ruma-federation-api?/unstable-msc3843"]
|
unstable-msc3843 = ["ruma-client-api?/unstable-msc3843", "ruma-federation-api?/unstable-msc3843"]
|
||||||
unstable-msc3916 = ["ruma-client-api?/unstable-msc3916"]
|
|
||||||
unstable-msc3927 = ["ruma-events?/unstable-msc3927"]
|
unstable-msc3927 = ["ruma-events?/unstable-msc3927"]
|
||||||
unstable-msc3930 = ["ruma-common/unstable-msc3930"]
|
unstable-msc3930 = ["ruma-common/unstable-msc3930"]
|
||||||
unstable-msc3931 = ["ruma-common/unstable-msc3931"]
|
unstable-msc3931 = ["ruma-common/unstable-msc3931"]
|
||||||
@ -268,6 +266,7 @@ unstable-msc4075 = ["ruma-events?/unstable-msc4075"]
|
|||||||
unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"]
|
unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"]
|
||||||
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
|
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
|
||||||
unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"]
|
unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"]
|
||||||
|
unstable-msc4140 = ["ruma-client-api?/unstable-msc4140"]
|
||||||
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
||||||
unstable-unspecified = [
|
unstable-unspecified = [
|
||||||
"ruma-common/unstable-unspecified",
|
"ruma-common/unstable-unspecified",
|
||||||
@ -285,7 +284,6 @@ __ci = [
|
|||||||
"unstable-msc2448",
|
"unstable-msc2448",
|
||||||
"unstable-msc2654",
|
"unstable-msc2654",
|
||||||
"unstable-msc2666",
|
"unstable-msc2666",
|
||||||
"unstable-msc2705",
|
|
||||||
"unstable-msc2747",
|
"unstable-msc2747",
|
||||||
"unstable-msc2867",
|
"unstable-msc2867",
|
||||||
"unstable-msc2870",
|
"unstable-msc2870",
|
||||||
@ -297,10 +295,10 @@ __ci = [
|
|||||||
"unstable-msc3245-v1-compat",
|
"unstable-msc3245-v1-compat",
|
||||||
"unstable-msc3246",
|
"unstable-msc3246",
|
||||||
"unstable-msc3266",
|
"unstable-msc3266",
|
||||||
"unstable-msc3291",
|
|
||||||
"unstable-msc3381",
|
"unstable-msc3381",
|
||||||
"unstable-msc3401",
|
"unstable-msc3401",
|
||||||
"unstable-msc3488",
|
"unstable-msc3488",
|
||||||
|
"unstable-msc3489",
|
||||||
"unstable-msc3551",
|
"unstable-msc3551",
|
||||||
"unstable-msc3552",
|
"unstable-msc3552",
|
||||||
"unstable-msc3553",
|
"unstable-msc3553",
|
||||||
@ -310,7 +308,6 @@ __ci = [
|
|||||||
"unstable-msc3723",
|
"unstable-msc3723",
|
||||||
"unstable-msc3814",
|
"unstable-msc3814",
|
||||||
"unstable-msc3843",
|
"unstable-msc3843",
|
||||||
"unstable-msc3916",
|
|
||||||
"unstable-msc3927",
|
"unstable-msc3927",
|
||||||
"unstable-msc3930",
|
"unstable-msc3930",
|
||||||
"unstable-msc3931",
|
"unstable-msc3931",
|
||||||
@ -323,6 +320,7 @@ __ci = [
|
|||||||
"unstable-msc4108",
|
"unstable-msc4108",
|
||||||
"unstable-msc4121",
|
"unstable-msc4121",
|
||||||
"unstable-msc4125",
|
"unstable-msc4125",
|
||||||
|
"unstable-msc4140"
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -17,7 +17,7 @@ const OLD_URL_WHITELIST: &[&str] =
|
|||||||
|
|
||||||
/// Authorized versions in URLs pointing to the new specs.
|
/// Authorized versions in URLs pointing to the new specs.
|
||||||
const NEW_VERSION_WHITELIST: &[&str] = &[
|
const NEW_VERSION_WHITELIST: &[&str] = &[
|
||||||
"v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7", "v1.8", "v1.9", "v1.10",
|
"v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7", "v1.8", "v1.9", "v1.10", "v1.11",
|
||||||
"latest",
|
"latest",
|
||||||
// This should only be enabled if a legitimate use case is found.
|
// This should only be enabled if a legitimate use case is found.
|
||||||
// "unstable",
|
// "unstable",
|
||||||
@ -220,48 +220,72 @@ fn get_page_ids(url: &str) -> Result<HashMap<String, HasDuplicates>> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For the URLs using the "latest" version, log the actual version we got.
|
||||||
|
if url[URL_PREFIX.len()..].starts_with("latest/") {
|
||||||
|
// Let's use the `meta` element with the `og:url` property, it contains the original
|
||||||
|
// relative URL of the page.
|
||||||
|
if tag.name.0 == b"meta"
|
||||||
|
&& tag
|
||||||
|
.attributes
|
||||||
|
.get(b"property".as_slice())
|
||||||
|
.is_some_and(|value| value.0 == b"og:url")
|
||||||
|
{
|
||||||
|
match tag.attributes.get(b"content".as_slice()) {
|
||||||
|
Some(value) => {
|
||||||
|
println!(
|
||||||
|
"Original URL for latest spec page: {}",
|
||||||
|
String::from_utf8_lossy(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => println!(
|
||||||
|
"Could not get original URL for latest spec page: /{}",
|
||||||
|
&url[URL_PREFIX.len()..]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let Some(id) =
|
let Some(id) =
|
||||||
tag.attributes.get(b"id".as_slice()).and_then(|s| String::from_utf8(s.0.clone()).ok())
|
tag.attributes.get(b"id".as_slice()).and_then(|s| String::from_utf8(s.0.clone()).ok())
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (id, has_duplicates) = uniquify_heading_id(id, &mut ids);
|
let has_duplicates = heading_id_has_duplicates(&id, &mut ids);
|
||||||
|
|
||||||
ids.insert(id, has_duplicates);
|
ids.insert(id, has_duplicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make sure the ID is unique in the page, if not make it unique.
|
/// Check whether the given heading ID has duplicates in the given map.
|
||||||
///
|
///
|
||||||
/// This is necessary because Matrix spec pages do that in JavaScript, so IDs
|
/// This check is necessary because duplicates IDs have a number depending on their occurrence in a
|
||||||
/// are not unique in the source.
|
/// HTML page. If a duplicate ID is added, moved or removed from the spec, its number might change
|
||||||
///
|
/// from one version to the next.
|
||||||
/// This is a reimplementation of the algorithm used for the spec.
|
fn heading_id_has_duplicates(
|
||||||
///
|
id: &str,
|
||||||
/// See <https://github.com/matrix-org/matrix-spec/blob/6b02e393082570db2d0a651ddb79a365bc4a0f8d/static/js/toc.js#L25-L37>.
|
|
||||||
fn uniquify_heading_id(
|
|
||||||
mut id: String,
|
|
||||||
unique_ids: &mut HashMap<String, HasDuplicates>,
|
unique_ids: &mut HashMap<String, HasDuplicates>,
|
||||||
) -> (String, HasDuplicates) {
|
) -> HasDuplicates {
|
||||||
let base_id = id.clone();
|
// IDs that should be duplicates end with `-{number}`.
|
||||||
let mut counter: u16 = 0;
|
let Some((start, _end)) =
|
||||||
let mut has_duplicates = HasDuplicates::No;
|
id.rsplit_once('-').filter(|(_start, end)| end.chars().all(|c| c.is_ascii_digit()))
|
||||||
|
else {
|
||||||
|
return HasDuplicates::No;
|
||||||
|
};
|
||||||
|
|
||||||
while let Some(other_id_has_dup) = unique_ids.get_mut(&id) {
|
// Update the first duplicate ID, because it doesn't end with a number.
|
||||||
has_duplicates = HasDuplicates::Yes;
|
if let Some(other_id_has_dup) = unique_ids.get_mut(start) {
|
||||||
*other_id_has_dup = HasDuplicates::Yes;
|
*other_id_has_dup = HasDuplicates::Yes;
|
||||||
counter += 1;
|
|
||||||
id = format!("{base_id}-{counter}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(id, has_duplicates)
|
HasDuplicates::Yes
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_link_err(error: &str, link: &SpecLink) {
|
fn print_link_err(error: &str, link: &SpecLink) {
|
||||||
println!(
|
println!(
|
||||||
"\n{error}\nfile: {}:{}\nlink: {}",
|
"\n{error}\n file: {}:{}\n link: {}",
|
||||||
link.path.display(),
|
link.path.display(),
|
||||||
link.line,
|
link.line,
|
||||||
link.url.get(..80).unwrap_or(&link.url),
|
link.url.get(..80).unwrap_or(&link.url),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user