Merge remote-tracking branch 'upstream/main' into conduwuit-changes
This commit is contained in:
commit
580eefe1c6
@ -16,6 +16,9 @@ Breaking changes:
|
|||||||
- `Error` is now non-exhaustive.
|
- `Error` is now non-exhaustive.
|
||||||
- `ErrorKind::Forbidden` is now a non-exhaustive struct variant that can be
|
- `ErrorKind::Forbidden` is now a non-exhaustive struct variant that can be
|
||||||
constructed with `ErrorKind::forbidden()`.
|
constructed with `ErrorKind::forbidden()`.
|
||||||
|
- The `retry_after_ms` field of `ErrorKind::LimitExceeded` was renamed to
|
||||||
|
`retry_after` and is now an `Option<RetryAfter>`, to add support for the
|
||||||
|
Retry-After header, according to MSC4041 / Matrix 1.10
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
@ -41,6 +44,7 @@ Improvements:
|
|||||||
- Add server support discovery endpoint, according to MSC1929 / Matrix 1.10
|
- Add server support discovery endpoint, according to MSC1929 / Matrix 1.10
|
||||||
- Add `dir` `Request` field on the `get_relating_events_with_rel_types` and
|
- Add `dir` `Request` field on the `get_relating_events_with_rel_types` and
|
||||||
`get_relating_events_with_rel_type_and_event_type` endpoints
|
`get_relating_events_with_rel_type_and_event_type` endpoints
|
||||||
|
- Add unstable support for moderator server support discovery, according to MSC4121
|
||||||
|
|
||||||
# 0.17.4
|
# 0.17.4
|
||||||
|
|
||||||
|
@ -47,11 +47,13 @@ unstable-msc3488 = []
|
|||||||
unstable-msc3575 = []
|
unstable-msc3575 = []
|
||||||
unstable-msc3814 = []
|
unstable-msc3814 = []
|
||||||
unstable-msc3983 = []
|
unstable-msc3983 = []
|
||||||
|
unstable-msc4121 = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
as_variant = { workspace = true }
|
as_variant = { workspace = true }
|
||||||
assign = { workspace = true }
|
assign = { workspace = true }
|
||||||
bytes = "1.0.1"
|
bytes = "1.0.1"
|
||||||
|
date_header = "1.0.5"
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
js_int = { workspace = true, features = ["serde"] }
|
js_int = { workspace = true, features = ["serde"] }
|
||||||
js_option = "0.1.1"
|
js_option = "0.1.1"
|
||||||
@ -61,6 +63,8 @@ ruma-events = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_html_form = { workspace = true }
|
serde_html_form = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
web-time = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_matches2 = { workspace = true }
|
assert_matches2 = { workspace = true }
|
||||||
|
@ -112,6 +112,13 @@ pub enum ContactRole {
|
|||||||
/// A role intended for sensitive requests.
|
/// A role intended for sensitive requests.
|
||||||
Security,
|
Security,
|
||||||
|
|
||||||
|
/// A role for moderation-related queries according to [MSC4121](https://github.com/matrix-org/matrix-spec-proposals/pull/4121).
|
||||||
|
///
|
||||||
|
/// The future prefix for this if accepted will be `m.role.moderator`
|
||||||
|
#[cfg(feature = "unstable-msc4121")]
|
||||||
|
#[ruma_enum(rename = "support.feline.msc4121.role.moderator", alias = "m.role.moderator")]
|
||||||
|
Moderator,
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
_Custom(PrivOwnedStr),
|
_Custom(PrivOwnedStr),
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! Errors that can be sent from the homeserver.
|
//! Errors that can be sent from the homeserver.
|
||||||
|
|
||||||
use std::{collections::BTreeMap, fmt, sync::Arc, time::Duration};
|
use std::{collections::BTreeMap, fmt, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use as_variant::as_variant;
|
use as_variant::as_variant;
|
||||||
use bytes::{BufMut, Bytes};
|
use bytes::{BufMut, Bytes};
|
||||||
@ -13,6 +13,8 @@ use ruma_common::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
|
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
|
||||||
|
use thiserror::Error;
|
||||||
|
use web_time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::PrivOwnedStr;
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
@ -59,8 +61,8 @@ pub enum ErrorKind {
|
|||||||
|
|
||||||
/// M_LIMIT_EXCEEDED
|
/// M_LIMIT_EXCEEDED
|
||||||
LimitExceeded {
|
LimitExceeded {
|
||||||
/// How long a client should wait in milliseconds before they can try again.
|
/// How long a client should wait before they can try again.
|
||||||
retry_after_ms: Option<Duration>,
|
retry_after: Option<RetryAfter>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// M_UNKNOWN
|
/// M_UNKNOWN
|
||||||
@ -350,19 +352,29 @@ impl EndpointError for Error {
|
|||||||
|
|
||||||
let body_bytes = &response.body().as_ref();
|
let body_bytes = &response.body().as_ref();
|
||||||
let error_body: ErrorBody = match from_json_slice(body_bytes) {
|
let error_body: ErrorBody = match from_json_slice(body_bytes) {
|
||||||
Ok(StandardErrorBody { kind, message }) => {
|
Ok(StandardErrorBody { mut kind, message }) => {
|
||||||
#[cfg(feature = "unstable-msc2967")]
|
let headers = response.headers();
|
||||||
let kind = if let ErrorKind::Forbidden { .. } = kind {
|
|
||||||
let authenticate = response
|
|
||||||
.headers()
|
|
||||||
.get(http::header::WWW_AUTHENTICATE)
|
|
||||||
.and_then(|val| val.to_str().ok())
|
|
||||||
.and_then(AuthenticateError::from_str);
|
|
||||||
|
|
||||||
ErrorKind::Forbidden { authenticate }
|
match &mut kind {
|
||||||
} else {
|
#[cfg(feature = "unstable-msc2967")]
|
||||||
kind
|
ErrorKind::Forbidden { authenticate } => {
|
||||||
};
|
*authenticate = headers
|
||||||
|
.get(http::header::WWW_AUTHENTICATE)
|
||||||
|
.and_then(|val| val.to_str().ok())
|
||||||
|
.and_then(AuthenticateError::from_str);
|
||||||
|
}
|
||||||
|
ErrorKind::LimitExceeded { retry_after } => {
|
||||||
|
// The Retry-After header takes precedence over the retry_after_ms field in
|
||||||
|
// the body.
|
||||||
|
if let Some(retry_after_header) = headers
|
||||||
|
.get(http::header::RETRY_AFTER)
|
||||||
|
.and_then(RetryAfter::from_header_value)
|
||||||
|
{
|
||||||
|
*retry_after = Some(retry_after_header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
ErrorBody::Standard { kind, message }
|
ErrorBody::Standard { kind, message }
|
||||||
}
|
}
|
||||||
@ -406,20 +418,24 @@ impl OutgoingResponse for Error {
|
|||||||
fn try_into_http_response<T: Default + BufMut>(
|
fn try_into_http_response<T: Default + BufMut>(
|
||||||
self,
|
self,
|
||||||
) -> Result<http::Response<T>, IntoHttpError> {
|
) -> Result<http::Response<T>, IntoHttpError> {
|
||||||
let builder = http::Response::builder()
|
let mut builder = http::Response::builder()
|
||||||
.header(http::header::CONTENT_TYPE, "application/json")
|
.header(http::header::CONTENT_TYPE, "application/json")
|
||||||
.status(self.status_code);
|
.status(self.status_code);
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc2967")]
|
#[allow(clippy::collapsible_match)]
|
||||||
let builder = if let ErrorBody::Standard {
|
if let ErrorBody::Standard { kind, .. } = &self.body {
|
||||||
kind: ErrorKind::Forbidden { authenticate: Some(auth_error) },
|
match kind {
|
||||||
..
|
#[cfg(feature = "unstable-msc2967")]
|
||||||
} = &self.body
|
ErrorKind::Forbidden { authenticate: Some(auth_error) } => {
|
||||||
{
|
builder = builder.header(http::header::WWW_AUTHENTICATE, auth_error);
|
||||||
builder.header(http::header::WWW_AUTHENTICATE, auth_error)
|
}
|
||||||
} else {
|
ErrorKind::LimitExceeded { retry_after: Some(retry_after) } => {
|
||||||
builder
|
let header_value = http::HeaderValue::try_from(retry_after)?;
|
||||||
};
|
builder = builder.header(http::header::RETRY_AFTER, header_value);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.body(match self.body {
|
.body(match self.body {
|
||||||
@ -532,6 +548,72 @@ impl TryFrom<&AuthenticateError> for http::HeaderValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How long a client should wait before it tries again.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(clippy::exhaustive_enums)]
|
||||||
|
pub enum RetryAfter {
|
||||||
|
/// The client should wait for the given duration.
|
||||||
|
///
|
||||||
|
/// This variant should be preferred for backwards compatibility, as it will also populate the
|
||||||
|
/// `retry_after_ms` field in the body of the response.
|
||||||
|
Delay(Duration),
|
||||||
|
/// The client should wait for the given date and time.
|
||||||
|
DateTime(SystemTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetryAfter {
|
||||||
|
fn from_header_value(value: &http::HeaderValue) -> Option<Self> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
|
||||||
|
if bytes.iter().all(|b| b.is_ascii_digit()) {
|
||||||
|
// It should be a duration.
|
||||||
|
Some(Self::Delay(Duration::from_secs(u64::from_str(value.to_str().ok()?).ok()?)))
|
||||||
|
} else {
|
||||||
|
// It should be a date.
|
||||||
|
let ts = date_header::parse(bytes).ok()?;
|
||||||
|
Some(Self::DateTime(UNIX_EPOCH.checked_add(Duration::from_secs(ts))?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&RetryAfter> for http::HeaderValue {
|
||||||
|
type Error = RetryAfterInvalidDateTime;
|
||||||
|
|
||||||
|
fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
|
||||||
|
RetryAfter::DateTime(time) => {
|
||||||
|
let mut buffer = [0; 29];
|
||||||
|
let duration =
|
||||||
|
time.duration_since(UNIX_EPOCH).map_err(|_| RetryAfterInvalidDateTime)?;
|
||||||
|
date_header::format(duration.as_secs(), &mut buffer)
|
||||||
|
.map_err(|_| RetryAfterInvalidDateTime)?;
|
||||||
|
let value = http::HeaderValue::from_bytes(&buffer)
|
||||||
|
.expect("date_header should produce a valid header value");
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error when converting a [`RetryAfter`] to a [`http::HeaderValue`].
|
||||||
|
///
|
||||||
|
/// Happens when the `DateTime` is too far in the past (before the Unix epoch) or the
|
||||||
|
/// future (after the year 9999).
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[allow(clippy::exhaustive_structs)]
|
||||||
|
#[error(
|
||||||
|
"Retry-After header serialization failed: the datetime is too far in the past or the future"
|
||||||
|
)]
|
||||||
|
pub struct RetryAfterInvalidDateTime;
|
||||||
|
|
||||||
|
impl From<RetryAfterInvalidDateTime> for IntoHttpError {
|
||||||
|
fn from(_value: RetryAfterInvalidDateTime) -> Self {
|
||||||
|
IntoHttpError::RetryAfterInvalidDatetime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
|
/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
|
||||||
pub trait FromHttpResponseErrorExt {
|
pub trait FromHttpResponseErrorExt {
|
||||||
/// If `self` is a server error in the `errcode` + `error` format expected
|
/// If `self` is a server error in the `errcode` + `error` format expected
|
||||||
@ -548,9 +630,13 @@ impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use serde_json::{from_value as from_json_value, json};
|
use ruma_common::api::{EndpointError, OutgoingResponse};
|
||||||
|
use serde_json::{
|
||||||
|
from_slice as from_json_slice, from_value as from_json_value, json, Value as JsonValue,
|
||||||
|
};
|
||||||
|
use web_time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
use super::{ErrorKind, StandardErrorBody};
|
use super::{Error, ErrorBody, ErrorKind, RetryAfter, StandardErrorBody};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_forbidden() {
|
fn deserialize_forbidden() {
|
||||||
@ -615,9 +701,7 @@ mod tests {
|
|||||||
#[cfg(feature = "unstable-msc2967")]
|
#[cfg(feature = "unstable-msc2967")]
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_insufficient_scope() {
|
fn deserialize_insufficient_scope() {
|
||||||
use ruma_common::api::EndpointError;
|
use super::AuthenticateError;
|
||||||
|
|
||||||
use super::{AuthenticateError, Error, ErrorBody};
|
|
||||||
|
|
||||||
let response = http::Response::builder()
|
let response = http::Response::builder()
|
||||||
.header(
|
.header(
|
||||||
@ -642,4 +726,223 @@ mod tests {
|
|||||||
assert_matches!(authenticate, Some(AuthenticateError::InsufficientScope { scope }));
|
assert_matches!(authenticate, Some(AuthenticateError::InsufficientScope { scope }));
|
||||||
assert_eq!(scope, "something_privileged");
|
assert_eq!(scope, "something_privileged");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_limit_exceeded_no_retry_after() {
|
||||||
|
let response = http::Response::builder()
|
||||||
|
.status(http::StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = Error::from_http_response(response);
|
||||||
|
|
||||||
|
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_matches!(
|
||||||
|
error.body,
|
||||||
|
ErrorBody::Standard { kind: ErrorKind::LimitExceeded { retry_after: None }, message }
|
||||||
|
);
|
||||||
|
assert_eq!(message, "Too many requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_limit_exceeded_retry_after_body() {
|
||||||
|
let response = http::Response::builder()
|
||||||
|
.status(http::StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
"retry_after_ms": 2000,
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = Error::from_http_response(response);
|
||||||
|
|
||||||
|
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_matches!(
|
||||||
|
error.body,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
|
||||||
|
message
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_matches!(retry_after, RetryAfter::Delay(delay));
|
||||||
|
assert_eq!(delay.as_millis(), 2000);
|
||||||
|
assert_eq!(message, "Too many requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_limit_exceeded_retry_after_header_delay() {
|
||||||
|
let response = http::Response::builder()
|
||||||
|
.status(http::StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.header(http::header::RETRY_AFTER, "2")
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = Error::from_http_response(response);
|
||||||
|
|
||||||
|
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_matches!(
|
||||||
|
error.body,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
|
||||||
|
message
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_matches!(retry_after, RetryAfter::Delay(delay));
|
||||||
|
assert_eq!(delay.as_millis(), 2000);
|
||||||
|
assert_eq!(message, "Too many requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_limit_exceeded_retry_after_header_datetime() {
|
||||||
|
let response = http::Response::builder()
|
||||||
|
.status(http::StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.header(http::header::RETRY_AFTER, "Fri, 15 May 2015 15:34:21 GMT")
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = Error::from_http_response(response);
|
||||||
|
|
||||||
|
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_matches!(
|
||||||
|
error.body,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
|
||||||
|
message
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_matches!(retry_after, RetryAfter::DateTime(time));
|
||||||
|
assert_eq!(time.duration_since(UNIX_EPOCH).unwrap().as_secs(), 1_431_704_061);
|
||||||
|
assert_eq!(message, "Too many requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_limit_exceeded_retry_after_header_over_body() {
|
||||||
|
let response = http::Response::builder()
|
||||||
|
.status(http::StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.header(http::header::RETRY_AFTER, "2")
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
"retry_after_ms": 3000,
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = Error::from_http_response(response);
|
||||||
|
|
||||||
|
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_matches!(
|
||||||
|
error.body,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
|
||||||
|
message
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_matches!(retry_after, RetryAfter::Delay(delay));
|
||||||
|
assert_eq!(delay.as_millis(), 2000);
|
||||||
|
assert_eq!(message, "Too many requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_limit_exceeded_retry_after_none() {
|
||||||
|
let error = Error::new(
|
||||||
|
http::StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded { retry_after: None },
|
||||||
|
message: "Too many requests".to_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_eq!(response.headers().get(http::header::RETRY_AFTER), None);
|
||||||
|
|
||||||
|
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
json_body,
|
||||||
|
json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_limit_exceeded_retry_after_delay() {
|
||||||
|
let error = Error::new(
|
||||||
|
http::StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded {
|
||||||
|
retry_after: Some(RetryAfter::Delay(Duration::from_secs(3))),
|
||||||
|
},
|
||||||
|
message: "Too many requests".to_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
|
||||||
|
assert_eq!(retry_after_header.to_str().unwrap(), "3");
|
||||||
|
|
||||||
|
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
json_body,
|
||||||
|
json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
"retry_after_ms": 3000,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_limit_exceeded_retry_after_datetime() {
|
||||||
|
let error = Error::new(
|
||||||
|
http::StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
ErrorBody::Standard {
|
||||||
|
kind: ErrorKind::LimitExceeded {
|
||||||
|
retry_after: Some(RetryAfter::DateTime(
|
||||||
|
UNIX_EPOCH + Duration::from_secs(1_431_704_061),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
message: "Too many requests".to_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
|
||||||
|
assert_eq!(retry_after_header.to_str().unwrap(), "Fri, 15 May 2015 15:34:21 GMT");
|
||||||
|
|
||||||
|
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
json_body,
|
||||||
|
json!({
|
||||||
|
"errcode": "M_LIMIT_EXCEEDED",
|
||||||
|
"error": "Too many requests",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ use serde::{
|
|||||||
};
|
};
|
||||||
use serde_json::from_value as from_json_value;
|
use serde_json::from_value as from_json_value;
|
||||||
|
|
||||||
use super::{ErrorKind, Extra};
|
use super::{ErrorKind, Extra, RetryAfter};
|
||||||
use crate::PrivOwnedStr;
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
enum Field<'de> {
|
enum Field<'de> {
|
||||||
@ -178,12 +178,13 @@ impl<'de> Visitor<'de> for ErrorKindVisitor {
|
|||||||
ErrCode::NotJson => ErrorKind::NotJson,
|
ErrCode::NotJson => ErrorKind::NotJson,
|
||||||
ErrCode::NotFound => ErrorKind::NotFound,
|
ErrCode::NotFound => ErrorKind::NotFound,
|
||||||
ErrCode::LimitExceeded => ErrorKind::LimitExceeded {
|
ErrCode::LimitExceeded => ErrorKind::LimitExceeded {
|
||||||
retry_after_ms: retry_after_ms
|
retry_after: retry_after_ms
|
||||||
.map(from_json_value::<UInt>)
|
.map(from_json_value::<UInt>)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(de::Error::custom)?
|
.map_err(de::Error::custom)?
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.map(Duration::from_millis),
|
.map(Duration::from_millis)
|
||||||
|
.map(RetryAfter::Delay),
|
||||||
},
|
},
|
||||||
ErrCode::Unknown => ErrorKind::Unknown,
|
ErrCode::Unknown => ErrorKind::Unknown,
|
||||||
ErrCode::Unrecognized => ErrorKind::Unrecognized,
|
ErrCode::Unrecognized => ErrorKind::Unrecognized,
|
||||||
@ -328,7 +329,7 @@ impl Serialize for ErrorKind {
|
|||||||
Self::UnknownToken { soft_logout: true } => {
|
Self::UnknownToken { soft_logout: true } => {
|
||||||
st.serialize_entry("soft_logout", &true)?;
|
st.serialize_entry("soft_logout", &true)?;
|
||||||
}
|
}
|
||||||
Self::LimitExceeded { retry_after_ms: Some(duration) } => {
|
Self::LimitExceeded { retry_after: Some(RetryAfter::Delay(duration)) } => {
|
||||||
st.serialize_entry(
|
st.serialize_entry(
|
||||||
"retry_after_ms",
|
"retry_after_ms",
|
||||||
&UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
|
&UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
|
||||||
|
@ -129,6 +129,13 @@ pub enum IntoHttpError {
|
|||||||
#[error("header serialization failed: {0}")]
|
#[error("header serialization failed: {0}")]
|
||||||
Header(#[from] http::header::InvalidHeaderValue),
|
Header(#[from] http::header::InvalidHeaderValue),
|
||||||
|
|
||||||
|
/// Retry-After header serialization failed because the datetime provided is after the year
|
||||||
|
/// 9999.
|
||||||
|
#[error(
|
||||||
|
"Retry-After header serialization failed: the year of the datetime is bigger than 9999"
|
||||||
|
)]
|
||||||
|
RetryAfterInvalidDatetime,
|
||||||
|
|
||||||
/// HTTP request construction failed.
|
/// HTTP request construction failed.
|
||||||
#[error("HTTP request construction failed: {0}")]
|
#[error("HTTP request construction failed: {0}")]
|
||||||
Http(#[from] http::Error),
|
Http(#[from] http::Error),
|
||||||
|
@ -31,6 +31,7 @@ Improvements:
|
|||||||
- Implement `From<JoinRule>` for `SpaceRoomJoinRule`
|
- Implement `From<JoinRule>` for `SpaceRoomJoinRule`
|
||||||
- Add `filename` and `formatted` fields to media event contents to support media captions
|
- Add `filename` and `formatted` fields to media event contents to support media captions
|
||||||
as per [MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530) / Matrix 1.10
|
as per [MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530) / Matrix 1.10
|
||||||
|
- Add support for multi-stream VoIP, according to MSC3077 / Matrix 1.10
|
||||||
|
|
||||||
# 0.27.11
|
# 0.27.11
|
||||||
|
|
||||||
|
@ -14,8 +14,11 @@ pub mod notify;
|
|||||||
pub mod reject;
|
pub mod reject;
|
||||||
pub mod select_answer;
|
pub mod select_answer;
|
||||||
|
|
||||||
|
use ruma_macros::StringEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
/// A VoIP session description.
|
/// A VoIP session description.
|
||||||
///
|
///
|
||||||
/// This is the same type as WebRTC's [`RTCSessionDescriptionInit`].
|
/// This is the same type as WebRTC's [`RTCSessionDescriptionInit`].
|
||||||
@ -44,6 +47,41 @@ impl SessionDescription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Metadata about a VoIP stream.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct StreamMetadata {
|
||||||
|
/// The purpose of the stream.
|
||||||
|
pub purpose: StreamPurpose,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamMetadata {
|
||||||
|
/// Creates a new `StreamMetadata` with the given purpose.
|
||||||
|
pub fn new(purpose: StreamPurpose) -> Self {
|
||||||
|
Self { purpose }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The purpose of a VoIP stream.
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||||
|
#[derive(Clone, PartialEq, Eq, StringEnum)]
|
||||||
|
#[ruma_enum(rename_all = "m.lowercase")]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum StreamPurpose {
|
||||||
|
/// `m.usermedia`.
|
||||||
|
///
|
||||||
|
/// A stream that contains the webcam and/or microphone tracks.
|
||||||
|
UserMedia,
|
||||||
|
|
||||||
|
/// `m.screenshare`.
|
||||||
|
///
|
||||||
|
/// A stream with the screen-sharing tracks.
|
||||||
|
ScreenShare,
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
_Custom(PrivOwnedStr),
|
||||||
|
}
|
||||||
|
|
||||||
/// The capabilities of a client in a VoIP call.
|
/// The capabilities of a client in a VoIP call.
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
@ -2,13 +2,15 @@
|
|||||||
//!
|
//!
|
||||||
//! [`m.call.answer`]: https://spec.matrix.org/latest/client-server-api/#mcallanswer
|
//! [`m.call.answer`]: https://spec.matrix.org/latest/client-server-api/#mcallanswer
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use ruma_common::{OwnedVoipId, VoipVersionId};
|
use ruma_common::{OwnedVoipId, VoipVersionId};
|
||||||
use ruma_macros::EventContent;
|
use ruma_macros::EventContent;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
use super::CallCapabilities;
|
use super::CallCapabilities;
|
||||||
use super::SessionDescription;
|
use super::{SessionDescription, StreamMetadata};
|
||||||
|
|
||||||
/// The content of an `m.call.answer` event.
|
/// The content of an `m.call.answer` event.
|
||||||
///
|
///
|
||||||
@ -34,6 +36,12 @@ pub struct CallAnswerEventContent {
|
|||||||
/// The VoIP capabilities of the client.
|
/// The VoIP capabilities of the client.
|
||||||
#[serde(default, skip_serializing_if = "CallCapabilities::is_default")]
|
#[serde(default, skip_serializing_if = "CallCapabilities::is_default")]
|
||||||
pub capabilities: CallCapabilities,
|
pub capabilities: CallCapabilities,
|
||||||
|
|
||||||
|
/// **Added in VoIP version 1.** Metadata describing the streams that will be sent.
|
||||||
|
///
|
||||||
|
/// This is a map of stream ID to metadata about the stream.
|
||||||
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
pub sdp_stream_metadata: BTreeMap<String, StreamMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallAnswerEventContent {
|
impl CallAnswerEventContent {
|
||||||
@ -46,6 +54,7 @@ impl CallAnswerEventContent {
|
|||||||
version,
|
version,
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
capabilities: Default::default(),
|
capabilities: Default::default(),
|
||||||
|
sdp_stream_metadata: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +78,7 @@ impl CallAnswerEventContent {
|
|||||||
version: VoipVersionId::V1,
|
version: VoipVersionId::V1,
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
capabilities: Default::default(),
|
capabilities: Default::default(),
|
||||||
|
sdp_stream_metadata: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
//!
|
//!
|
||||||
//! [`m.call.invite`]: https://spec.matrix.org/latest/client-server-api/#mcallinvite
|
//! [`m.call.invite`]: https://spec.matrix.org/latest/client-server-api/#mcallinvite
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use js_int::UInt;
|
use js_int::UInt;
|
||||||
use ruma_common::{OwnedUserId, OwnedVoipId, VoipVersionId};
|
use ruma_common::{OwnedUserId, OwnedVoipId, VoipVersionId};
|
||||||
use ruma_macros::EventContent;
|
use ruma_macros::EventContent;
|
||||||
@ -9,7 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
use super::CallCapabilities;
|
use super::CallCapabilities;
|
||||||
use super::SessionDescription;
|
use super::{SessionDescription, StreamMetadata};
|
||||||
|
|
||||||
/// The content of an `m.call.invite` event.
|
/// The content of an `m.call.invite` event.
|
||||||
///
|
///
|
||||||
@ -49,6 +51,12 @@ pub struct CallInviteEventContent {
|
|||||||
/// The invite should be ignored if the invitee is set and doesn't match the user's ID.
|
/// The invite should be ignored if the invitee is set and doesn't match the user's ID.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub invitee: Option<OwnedUserId>,
|
pub invitee: Option<OwnedUserId>,
|
||||||
|
|
||||||
|
/// **Added in VoIP version 1.** Metadata describing the streams that will be sent.
|
||||||
|
///
|
||||||
|
/// This is a map of stream ID to metadata about the stream.
|
||||||
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
pub sdp_stream_metadata: BTreeMap<String, StreamMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallInviteEventContent {
|
impl CallInviteEventContent {
|
||||||
@ -69,6 +77,7 @@ impl CallInviteEventContent {
|
|||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
capabilities: Default::default(),
|
capabilities: Default::default(),
|
||||||
invitee: None,
|
invitee: None,
|
||||||
|
sdp_stream_metadata: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +104,7 @@ impl CallInviteEventContent {
|
|||||||
#[cfg(feature = "unstable-msc2747")]
|
#[cfg(feature = "unstable-msc2747")]
|
||||||
capabilities: Default::default(),
|
capabilities: Default::default(),
|
||||||
invitee: None,
|
invitee: None,
|
||||||
|
sdp_stream_metadata: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
//!
|
//!
|
||||||
//! [`m.call.negotiate`]: https://spec.matrix.org/latest/client-server-api/#mcallnegotiate
|
//! [`m.call.negotiate`]: https://spec.matrix.org/latest/client-server-api/#mcallnegotiate
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use js_int::UInt;
|
use js_int::UInt;
|
||||||
use ruma_common::{OwnedVoipId, VoipVersionId};
|
use ruma_common::{OwnedVoipId, VoipVersionId};
|
||||||
use ruma_macros::EventContent;
|
use ruma_macros::EventContent;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::SessionDescription;
|
use super::{SessionDescription, StreamMetadata};
|
||||||
|
|
||||||
/// **Added in VoIP version 1.** The content of an `m.call.negotiate` event.
|
/// **Added in VoIP version 1.** The content of an `m.call.negotiate` event.
|
||||||
///
|
///
|
||||||
@ -37,6 +39,12 @@ pub struct CallNegotiateEventContent {
|
|||||||
|
|
||||||
/// The session description of the negotiation.
|
/// The session description of the negotiation.
|
||||||
pub description: SessionDescription,
|
pub description: SessionDescription,
|
||||||
|
|
||||||
|
/// Metadata describing the streams that will be sent.
|
||||||
|
///
|
||||||
|
/// This is a map of stream ID to metadata about the stream.
|
||||||
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
pub sdp_stream_metadata: BTreeMap<String, StreamMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallNegotiateEventContent {
|
impl CallNegotiateEventContent {
|
||||||
@ -49,7 +57,14 @@ impl CallNegotiateEventContent {
|
|||||||
lifetime: UInt,
|
lifetime: UInt,
|
||||||
description: SessionDescription,
|
description: SessionDescription,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { call_id, party_id, version, lifetime, description }
|
Self {
|
||||||
|
call_id,
|
||||||
|
party_id,
|
||||||
|
version,
|
||||||
|
lifetime,
|
||||||
|
description,
|
||||||
|
sdp_stream_metadata: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience method to create a version 1 `CallNegotiateEventContent` with all the required
|
/// Convenience method to create a version 1 `CallNegotiateEventContent` with all the required
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Breaking changes:
|
||||||
|
|
||||||
|
* Use `RawValue` to represent body of `/v1/send_join` request, rather than incorrectly using
|
||||||
|
query parameters
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
* Implement `From<SpaceHierarchyParentSummary>` for `SpaceHierarchyChildSummary`
|
* Implement `From<SpaceHierarchyParentSummary>` for `SpaceHierarchyChildSummary`
|
||||||
|
@ -2,15 +2,12 @@
|
|||||||
//!
|
//!
|
||||||
//! [spec]: https://spec.matrix.org/latest/server-server-api/#put_matrixfederationv1send_leaveroomideventid
|
//! [spec]: https://spec.matrix.org/latest/server-server-api/#put_matrixfederationv1send_leaveroomideventid
|
||||||
|
|
||||||
use js_int::UInt;
|
|
||||||
use ruma_common::{
|
use ruma_common::{
|
||||||
api::{request, response, Metadata},
|
api::{request, response, Metadata},
|
||||||
metadata,
|
metadata, OwnedEventId, OwnedRoomId,
|
||||||
serde::Raw,
|
|
||||||
MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId,
|
|
||||||
};
|
};
|
||||||
use ruma_events::{room::member::RoomMemberEventContent, StateEventType};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::value::RawValue as RawJsonValue;
|
||||||
|
|
||||||
const METADATA: Metadata = metadata! {
|
const METADATA: Metadata = metadata! {
|
||||||
method: PUT,
|
method: PUT,
|
||||||
@ -26,6 +23,8 @@ const METADATA: Metadata = metadata! {
|
|||||||
#[request]
|
#[request]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// The room ID that is about to be left.
|
/// The room ID that is about to be left.
|
||||||
|
///
|
||||||
|
/// Do not use this. Instead, use the `room_id` field inside the PDU.
|
||||||
#[ruma_api(path)]
|
#[ruma_api(path)]
|
||||||
pub room_id: OwnedRoomId,
|
pub room_id: OwnedRoomId,
|
||||||
|
|
||||||
@ -33,34 +32,9 @@ pub struct Request {
|
|||||||
#[ruma_api(path)]
|
#[ruma_api(path)]
|
||||||
pub event_id: OwnedEventId,
|
pub event_id: OwnedEventId,
|
||||||
|
|
||||||
/// The user ID of the leaving member.
|
/// The PDU.
|
||||||
#[ruma_api(query)]
|
#[ruma_api(body)]
|
||||||
pub sender: OwnedUserId,
|
pub pdu: Box<RawJsonValue>,
|
||||||
|
|
||||||
/// The name of the leaving homeserver.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
pub origin: OwnedServerName,
|
|
||||||
|
|
||||||
/// A timestamp added by the leaving homeserver.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
|
|
||||||
|
|
||||||
/// The value `m.room.member`.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub event_type: StateEventType,
|
|
||||||
|
|
||||||
/// The user ID of the leaving member.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
pub state_key: String,
|
|
||||||
|
|
||||||
/// The content of the event.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
pub content: Raw<RoomMemberEventContent>,
|
|
||||||
|
|
||||||
/// This field must be present but is ignored; it may be 0.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
pub depth: UInt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response type for the `create_leave_event` endpoint.
|
/// Response type for the `create_leave_event` endpoint.
|
||||||
@ -75,66 +49,10 @@ pub struct Response {
|
|||||||
pub empty: Empty,
|
pub empty: Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initial set of fields of `Request`.
|
impl Request {
|
||||||
///
|
/// Creates a new `Request` from the given room ID, event ID and PDU.
|
||||||
/// This struct will not be updated even if additional fields are added to `Request` in a
|
pub fn new(room_id: OwnedRoomId, event_id: OwnedEventId, pdu: Box<RawJsonValue>) -> Self {
|
||||||
/// new (non-breaking) release of the Matrix specification.
|
Self { room_id, event_id, pdu }
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(clippy::exhaustive_structs)]
|
|
||||||
pub struct RequestInit {
|
|
||||||
/// The room ID that is about to be left.
|
|
||||||
pub room_id: OwnedRoomId,
|
|
||||||
|
|
||||||
/// The event ID for the leave event.
|
|
||||||
pub event_id: OwnedEventId,
|
|
||||||
|
|
||||||
/// The user ID of the leaving member.
|
|
||||||
pub sender: OwnedUserId,
|
|
||||||
|
|
||||||
/// The name of the leaving homeserver.
|
|
||||||
pub origin: OwnedServerName,
|
|
||||||
|
|
||||||
/// A timestamp added by the leaving homeserver.
|
|
||||||
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
|
|
||||||
|
|
||||||
/// The value `m.room.member`.
|
|
||||||
pub event_type: StateEventType,
|
|
||||||
|
|
||||||
/// The user ID of the leaving member.
|
|
||||||
pub state_key: String,
|
|
||||||
|
|
||||||
/// The content of the event.
|
|
||||||
pub content: Raw<RoomMemberEventContent>,
|
|
||||||
|
|
||||||
/// This field must be present but is ignored; it may be 0.
|
|
||||||
pub depth: UInt,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RequestInit> for Request {
|
|
||||||
/// Creates a new `Request` from `RequestInit`.
|
|
||||||
fn from(init: RequestInit) -> Self {
|
|
||||||
let RequestInit {
|
|
||||||
room_id,
|
|
||||||
event_id,
|
|
||||||
sender,
|
|
||||||
origin,
|
|
||||||
origin_server_ts,
|
|
||||||
event_type,
|
|
||||||
state_key,
|
|
||||||
content,
|
|
||||||
depth,
|
|
||||||
} = init;
|
|
||||||
Self {
|
|
||||||
room_id,
|
|
||||||
event_id,
|
|
||||||
sender,
|
|
||||||
origin,
|
|
||||||
origin_server_ts,
|
|
||||||
event_type,
|
|
||||||
state_key,
|
|
||||||
content,
|
|
||||||
depth,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
- Add support for deprecated HTML tags, according to Matrix 1.10
|
||||||
|
|
||||||
# 0.1.0
|
# 0.1.0
|
||||||
|
|
||||||
Initial release
|
Initial release
|
||||||
|
@ -16,7 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
as_variant = { workspace = true }
|
as_variant = { workspace = true }
|
||||||
html5ever = "0.26.0"
|
html5ever = "0.27.0"
|
||||||
phf = { version = "0.11.1", features = ["macros"] }
|
phf = { version = "0.11.1", features = ["macros"] }
|
||||||
tracing = { workspace = true, features = ["attributes"] }
|
tracing = { workspace = true, features = ["attributes"] }
|
||||||
wildmatch = "2.0.0"
|
wildmatch = "2.0.0"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use html5ever::{tendril::StrTendril, Attribute};
|
use html5ever::{tendril::StrTendril, Attribute, LocalName};
|
||||||
use phf::{phf_map, phf_set, Map, Set};
|
use phf::{phf_map, phf_set, Map, Set};
|
||||||
use wildmatch::WildMatch;
|
use wildmatch::WildMatch;
|
||||||
|
|
||||||
@ -12,11 +12,22 @@ pub struct SanitizerConfig {
|
|||||||
/// If this is `None`, all tags are allowed.
|
/// If this is `None`, all tags are allowed.
|
||||||
allowed_tags: Option<&'static Set<&'static str>>,
|
allowed_tags: Option<&'static Set<&'static str>>,
|
||||||
|
|
||||||
|
/// The allowed deprecated HTML tags.
|
||||||
|
///
|
||||||
|
/// This is a map of allowed deprecated tag to their replacement tag.
|
||||||
|
deprecated_tags: Option<&'static Map<&'static str, &'static str>>,
|
||||||
|
|
||||||
/// The allowed attributes per tag.
|
/// The allowed attributes per tag.
|
||||||
///
|
///
|
||||||
/// If this is `None`, all attributes are allowed.
|
/// If this is `None`, all attributes are allowed.
|
||||||
allowed_attrs: Option<&'static Map<&'static str, &'static Set<&'static str>>>,
|
allowed_attrs: Option<&'static Map<&'static str, &'static Set<&'static str>>>,
|
||||||
|
|
||||||
|
/// The allowed deprecated attributes per tag.
|
||||||
|
///
|
||||||
|
/// This is a map of tag to a map of allowed deprecated attribute to their replacement
|
||||||
|
/// attribute.
|
||||||
|
deprecated_attrs: Option<&'static Map<&'static str, &'static Map<&'static str, &'static str>>>,
|
||||||
|
|
||||||
/// The allowed URI schemes per tag.
|
/// The allowed URI schemes per tag.
|
||||||
///
|
///
|
||||||
/// If this is `None`, all schemes are allowed.
|
/// If this is `None`, all schemes are allowed.
|
||||||
@ -43,13 +54,17 @@ impl SanitizerConfig {
|
|||||||
/// Constructs a `SanitizerConfig` that will filter tags or attributes not [listed in the
|
/// Constructs a `SanitizerConfig` that will filter tags or attributes not [listed in the
|
||||||
/// Matrix specification].
|
/// Matrix specification].
|
||||||
///
|
///
|
||||||
|
/// Deprecated tags will be replaced with their non-deprecated equivalent.
|
||||||
|
///
|
||||||
/// It will not remove the reply fallback by default.
|
/// It will not remove the reply fallback by default.
|
||||||
///
|
///
|
||||||
/// [listed in the Matrix specification]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
|
/// [listed in the Matrix specification]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
|
||||||
pub fn strict() -> Self {
|
pub fn strict() -> Self {
|
||||||
Self {
|
Self {
|
||||||
allowed_tags: Some(&ALLOWED_TAGS_WITHOUT_REPLY_STRICT),
|
allowed_tags: Some(&ALLOWED_TAGS_WITHOUT_REPLY_STRICT),
|
||||||
|
deprecated_tags: Some(&DEPRECATED_TAGS),
|
||||||
allowed_attrs: Some(&ALLOWED_ATTRIBUTES_STRICT),
|
allowed_attrs: Some(&ALLOWED_ATTRIBUTES_STRICT),
|
||||||
|
deprecated_attrs: Some(&DEPRECATED_ATTRS),
|
||||||
allowed_schemes: Some(&ALLOWED_SCHEMES_STRICT),
|
allowed_schemes: Some(&ALLOWED_SCHEMES_STRICT),
|
||||||
allowed_classes: Some(&ALLOWED_CLASSES_STRICT),
|
allowed_classes: Some(&ALLOWED_CLASSES_STRICT),
|
||||||
max_depth: Some(MAX_DEPTH_STRICT),
|
max_depth: Some(MAX_DEPTH_STRICT),
|
||||||
@ -62,6 +77,8 @@ impl SanitizerConfig {
|
|||||||
///
|
///
|
||||||
/// - The `matrix` scheme is allowed in links.
|
/// - The `matrix` scheme is allowed in links.
|
||||||
///
|
///
|
||||||
|
/// Deprecated tags will be replaced with their non-deprecated equivalent.
|
||||||
|
///
|
||||||
/// It will not remove the reply fallback by default.
|
/// It will not remove the reply fallback by default.
|
||||||
///
|
///
|
||||||
/// [listed in the Matrix specification]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
|
/// [listed in the Matrix specification]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
|
||||||
@ -89,6 +106,8 @@ impl SanitizerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clean_node(&self, html: &mut Html, node_id: usize, depth: u32) {
|
fn clean_node(&self, html: &mut Html, node_id: usize, depth: u32) {
|
||||||
|
self.apply_deprecations(html, node_id);
|
||||||
|
|
||||||
let action = self.node_action(html, node_id, depth);
|
let action = self.node_action(html, node_id, depth);
|
||||||
|
|
||||||
if action != NodeAction::Remove {
|
if action != NodeAction::Remove {
|
||||||
@ -111,6 +130,42 @@ impl SanitizerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_deprecations(&self, html: &mut Html, node_id: usize) {
|
||||||
|
if let NodeData::Element(ElementData { name, attrs, .. }) = &mut html.nodes[node_id].data {
|
||||||
|
let tag: &str = &name.local;
|
||||||
|
|
||||||
|
if let Some(deprecated_attrs) =
|
||||||
|
self.deprecated_attrs.and_then(|deprecated_attrs| deprecated_attrs.get(tag))
|
||||||
|
{
|
||||||
|
*attrs = attrs
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut attr| {
|
||||||
|
let attr_name: &str = &attr.name.local;
|
||||||
|
|
||||||
|
let attr_replacement =
|
||||||
|
deprecated_attrs.get(attr_name).map(|s| LocalName::from(*s));
|
||||||
|
|
||||||
|
if let Some(attr_replacement) = attr_replacement {
|
||||||
|
attr.name.local = attr_replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
attr
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag_replacement = self
|
||||||
|
.deprecated_tags
|
||||||
|
.and_then(|deprecated_tags| deprecated_tags.get(tag))
|
||||||
|
.map(|s| LocalName::from(*s));
|
||||||
|
|
||||||
|
if let Some(tag_replacement) = tag_replacement {
|
||||||
|
name.local = tag_replacement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn node_action(&self, html: &Html, node_id: usize, depth: u32) -> NodeAction {
|
fn node_action(&self, html: &Html, node_id: usize, depth: u32) -> NodeAction {
|
||||||
match &html.nodes[node_id].data {
|
match &html.nodes[node_id].data {
|
||||||
NodeData::Element(ElementData { name, attrs, .. }) => {
|
NodeData::Element(ElementData { name, attrs, .. }) => {
|
||||||
@ -247,8 +302,8 @@ enum AttributeAction {
|
|||||||
|
|
||||||
/// List of HTML tags allowed in the Matrix specification, without the rich reply fallback tag.
|
/// List of HTML tags allowed in the Matrix specification, without the rich reply fallback tag.
|
||||||
static ALLOWED_TAGS_WITHOUT_REPLY_STRICT: Set<&str> = phf_set! {
|
static ALLOWED_TAGS_WITHOUT_REPLY_STRICT: Set<&str> = phf_set! {
|
||||||
"font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a",
|
"del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a",
|
||||||
"ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em", "strike",
|
"ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em", "s",
|
||||||
"code", "hr", "br", "div", "table", "thead", "tbody", "tr", "th", "td",
|
"code", "hr", "br", "div", "table", "thead", "tbody", "tr", "th", "td",
|
||||||
"caption", "pre", "span", "img", "details", "summary",
|
"caption", "pre", "span", "img", "details", "summary",
|
||||||
};
|
};
|
||||||
@ -256,17 +311,20 @@ static ALLOWED_TAGS_WITHOUT_REPLY_STRICT: Set<&str> = phf_set! {
|
|||||||
/// The HTML tag name for a rich reply fallback.
|
/// The HTML tag name for a rich reply fallback.
|
||||||
const RICH_REPLY_TAG: &str = "mx-reply";
|
const RICH_REPLY_TAG: &str = "mx-reply";
|
||||||
|
|
||||||
|
/// HTML tags that were allowed in the Matrix specification, with their replacement.
|
||||||
|
static DEPRECATED_TAGS: Map<&str, &str> = phf_map! {
|
||||||
|
"font" => "span",
|
||||||
|
"strike" => "s",
|
||||||
|
};
|
||||||
|
|
||||||
/// Allowed attributes per HTML tag according to the Matrix specification.
|
/// Allowed attributes per HTML tag according to the Matrix specification.
|
||||||
static ALLOWED_ATTRIBUTES_STRICT: Map<&str, &Set<&str>> = phf_map! {
|
static ALLOWED_ATTRIBUTES_STRICT: Map<&str, &Set<&str>> = phf_map! {
|
||||||
"font" => &ALLOWED_ATTRIBUTES_FONT_STRICT,
|
|
||||||
"span" => &ALLOWED_ATTRIBUTES_SPAN_STRICT,
|
"span" => &ALLOWED_ATTRIBUTES_SPAN_STRICT,
|
||||||
"a" => &ALLOWED_ATTRIBUTES_A_STRICT,
|
"a" => &ALLOWED_ATTRIBUTES_A_STRICT,
|
||||||
"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,
|
||||||
};
|
};
|
||||||
static ALLOWED_ATTRIBUTES_FONT_STRICT: Set<&str> =
|
|
||||||
phf_set! { "data-mx-bg-color", "data-mx-color", "color" };
|
|
||||||
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" };
|
||||||
static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" };
|
static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" };
|
||||||
@ -275,6 +333,13 @@ static ALLOWED_ATTRIBUTES_IMG_STRICT: Set<&str> =
|
|||||||
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" };
|
||||||
|
|
||||||
|
/// Attributes that were allowed on HTML tags according to the Matrix specification, with their
|
||||||
|
/// replacement.
|
||||||
|
static DEPRECATED_ATTRS: Map<&str, &Map<&str, &str>> = phf_map! {
|
||||||
|
"font" => &DEPRECATED_ATTRIBUTES_FONT,
|
||||||
|
};
|
||||||
|
static DEPRECATED_ATTRIBUTES_FONT: Map<&str, &str> = phf_map! { "color" => "data-mx-color" };
|
||||||
|
|
||||||
/// Allowed schemes of URIs per HTML tag and attribute tuple according to the Matrix specification.
|
/// Allowed schemes of URIs per HTML tag and attribute tuple according to the Matrix specification.
|
||||||
static ALLOWED_SCHEMES_STRICT: Map<&str, &Set<&str>> = phf_map! {
|
static ALLOWED_SCHEMES_STRICT: Map<&str, &Set<&str>> = phf_map! {
|
||||||
"a:href" => &ALLOWED_SCHEMES_A_HREF_STRICT,
|
"a:href" => &ALLOWED_SCHEMES_A_HREF_STRICT,
|
||||||
|
@ -122,7 +122,7 @@ fn attrs_remove() {
|
|||||||
let mut html = Html::parse(
|
let mut html = Html::parse(
|
||||||
"\
|
"\
|
||||||
<h1 id=\"anchor1\">Title for important stuff</h1>\
|
<h1 id=\"anchor1\">Title for important stuff</h1>\
|
||||||
<p class=\"important\">Look at <font color=\"blue\" size=20>me!</font></p>\
|
<p class=\"important\">Look at <span data-mx-color=\"#0000ff\" size=20>me!</span></p>\
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
html.sanitize_with(config);
|
html.sanitize_with(config);
|
||||||
@ -131,7 +131,7 @@ fn attrs_remove() {
|
|||||||
html.to_string(),
|
html.to_string(),
|
||||||
"\
|
"\
|
||||||
<h1>Title for important stuff</h1>\
|
<h1>Title for important stuff</h1>\
|
||||||
<p>Look at <font color=\"blue\">me!</font></p>\
|
<p>Look at <span data-mx-color=\"#0000ff\">me!</span></p>\
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -246,3 +246,21 @@ fn depth_remove() {
|
|||||||
assert!(res.contains("I should be fine."));
|
assert!(res.contains("I should be fine."));
|
||||||
assert!(!res.contains("I am in too deep!"));
|
assert!(!res.contains("I am in too deep!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_deprecated() {
|
||||||
|
let config = SanitizerConfig::strict();
|
||||||
|
let mut html = Html::parse(
|
||||||
|
"\
|
||||||
|
<p>Look at <strike>you </strike><font data-mx-bg-color=\"#ff0000\" color=\"#0000ff\">me!</span></p>\
|
||||||
|
",
|
||||||
|
);
|
||||||
|
html.sanitize_with(config);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
html.to_string(),
|
||||||
|
"\
|
||||||
|
<p>Look at <s>you </s><span data-mx-bg-color=\"#ff0000\" data-mx-color=\"#0000ff\">me!</span></p>\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -36,6 +36,8 @@ pub enum RenameRule {
|
|||||||
/// Rename direct children to "M_MATRIX_ERROR_CASE" style, as used for responses with error in
|
/// Rename direct children to "M_MATRIX_ERROR_CASE" style, as used for responses with error in
|
||||||
/// Matrix spec.
|
/// Matrix spec.
|
||||||
MatrixErrorCase,
|
MatrixErrorCase,
|
||||||
|
/// Rename the direct children to "m.lowercase" style.
|
||||||
|
MatrixLowerCase,
|
||||||
/// Rename the direct children to "m.snake_case" style.
|
/// Rename the direct children to "m.snake_case" style.
|
||||||
MatrixSnakeCase,
|
MatrixSnakeCase,
|
||||||
/// Rename the direct children to "m.dotted.case" style.
|
/// Rename the direct children to "m.dotted.case" style.
|
||||||
@ -68,6 +70,7 @@ impl RenameRule {
|
|||||||
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
|
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
|
||||||
ScreamingKebabCase => ScreamingSnakeCase.apply_to_variant(variant).replace('_', "-"),
|
ScreamingKebabCase => ScreamingSnakeCase.apply_to_variant(variant).replace('_', "-"),
|
||||||
MatrixErrorCase => String::from("M_") + &ScreamingSnakeCase.apply_to_variant(variant),
|
MatrixErrorCase => String::from("M_") + &ScreamingSnakeCase.apply_to_variant(variant),
|
||||||
|
MatrixLowerCase => String::from("m.") + &LowerCase.apply_to_variant(variant),
|
||||||
MatrixSnakeCase => String::from("m.") + &SnakeCase.apply_to_variant(variant),
|
MatrixSnakeCase => String::from("m.") + &SnakeCase.apply_to_variant(variant),
|
||||||
MatrixDottedCase => {
|
MatrixDottedCase => {
|
||||||
String::from("m.") + &SnakeCase.apply_to_variant(variant).replace('_', ".")
|
String::from("m.") + &SnakeCase.apply_to_variant(variant).replace('_', ".")
|
||||||
@ -106,6 +109,7 @@ impl RenameRule {
|
|||||||
KebabCase => field.replace('_', "-"),
|
KebabCase => field.replace('_', "-"),
|
||||||
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
|
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
|
||||||
MatrixErrorCase => String::from("M_") + &ScreamingSnakeCase.apply_to_field(field),
|
MatrixErrorCase => String::from("M_") + &ScreamingSnakeCase.apply_to_field(field),
|
||||||
|
MatrixLowerCase => String::from("m.") + field,
|
||||||
MatrixSnakeCase => String::from("m.") + field,
|
MatrixSnakeCase => String::from("m.") + field,
|
||||||
MatrixDottedCase => String::from("m.") + &field.replace('_', "."),
|
MatrixDottedCase => String::from("m.") + &field.replace('_', "."),
|
||||||
MatrixRuleSnakeCase => String::from(".m.rule.") + field,
|
MatrixRuleSnakeCase => String::from(".m.rule.") + field,
|
||||||
@ -129,6 +133,7 @@ impl FromStr for RenameRule {
|
|||||||
"SCREAMING-KEBAB-CASE" => Ok(ScreamingKebabCase),
|
"SCREAMING-KEBAB-CASE" => Ok(ScreamingKebabCase),
|
||||||
"M_MATRIX_ERROR_CASE" => Ok(MatrixErrorCase),
|
"M_MATRIX_ERROR_CASE" => Ok(MatrixErrorCase),
|
||||||
"m.snake_case" => Ok(MatrixSnakeCase),
|
"m.snake_case" => Ok(MatrixSnakeCase),
|
||||||
|
"m.lowercase" => Ok(MatrixLowerCase),
|
||||||
"m.dotted.case" => Ok(MatrixDottedCase),
|
"m.dotted.case" => Ok(MatrixDottedCase),
|
||||||
".m.rule.snake_case" => Ok(MatrixRuleSnakeCase),
|
".m.rule.snake_case" => Ok(MatrixRuleSnakeCase),
|
||||||
"m.role.snake_case" => Ok(MatrixRoleSnakeCase),
|
"m.role.snake_case" => Ok(MatrixRoleSnakeCase),
|
||||||
@ -149,6 +154,7 @@ fn rename_variants() {
|
|||||||
kebab,
|
kebab,
|
||||||
screaming_kebab,
|
screaming_kebab,
|
||||||
matrix_error,
|
matrix_error,
|
||||||
|
m_lower,
|
||||||
m_snake,
|
m_snake,
|
||||||
m_dotted,
|
m_dotted,
|
||||||
m_rule_snake,
|
m_rule_snake,
|
||||||
@ -166,6 +172,7 @@ fn rename_variants() {
|
|||||||
"M_OUTCOME",
|
"M_OUTCOME",
|
||||||
"m.outcome",
|
"m.outcome",
|
||||||
"m.outcome",
|
"m.outcome",
|
||||||
|
"m.outcome",
|
||||||
".m.rule.outcome",
|
".m.rule.outcome",
|
||||||
"m.role.outcome",
|
"m.role.outcome",
|
||||||
),
|
),
|
||||||
@ -179,12 +186,28 @@ fn rename_variants() {
|
|||||||
"very-tasty",
|
"very-tasty",
|
||||||
"VERY-TASTY",
|
"VERY-TASTY",
|
||||||
"M_VERY_TASTY",
|
"M_VERY_TASTY",
|
||||||
|
"m.verytasty",
|
||||||
"m.very_tasty",
|
"m.very_tasty",
|
||||||
"m.very.tasty",
|
"m.very.tasty",
|
||||||
".m.rule.very_tasty",
|
".m.rule.very_tasty",
|
||||||
"m.role.very_tasty",
|
"m.role.very_tasty",
|
||||||
),
|
),
|
||||||
("A", "a", "A", "a", "a", "A", "a", "A", "M_A", "m.a", "m.a", ".m.rule.a", "m.role.a"),
|
(
|
||||||
|
"A",
|
||||||
|
"a",
|
||||||
|
"A",
|
||||||
|
"a",
|
||||||
|
"a",
|
||||||
|
"A",
|
||||||
|
"a",
|
||||||
|
"A",
|
||||||
|
"M_A",
|
||||||
|
"m.a",
|
||||||
|
"m.a",
|
||||||
|
"m.a",
|
||||||
|
".m.rule.a",
|
||||||
|
"m.role.a",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"Z42",
|
"Z42",
|
||||||
"z42",
|
"z42",
|
||||||
@ -197,6 +220,7 @@ fn rename_variants() {
|
|||||||
"M_Z42",
|
"M_Z42",
|
||||||
"m.z42",
|
"m.z42",
|
||||||
"m.z42",
|
"m.z42",
|
||||||
|
"m.z42",
|
||||||
".m.rule.z42",
|
".m.rule.z42",
|
||||||
"m.role.z42",
|
"m.role.z42",
|
||||||
),
|
),
|
||||||
@ -211,6 +235,7 @@ fn rename_variants() {
|
|||||||
assert_eq!(KebabCase.apply_to_variant(original), kebab);
|
assert_eq!(KebabCase.apply_to_variant(original), kebab);
|
||||||
assert_eq!(ScreamingKebabCase.apply_to_variant(original), screaming_kebab);
|
assert_eq!(ScreamingKebabCase.apply_to_variant(original), screaming_kebab);
|
||||||
assert_eq!(MatrixErrorCase.apply_to_variant(original), matrix_error);
|
assert_eq!(MatrixErrorCase.apply_to_variant(original), matrix_error);
|
||||||
|
assert_eq!(MatrixLowerCase.apply_to_variant(original), m_lower);
|
||||||
assert_eq!(MatrixSnakeCase.apply_to_variant(original), m_snake);
|
assert_eq!(MatrixSnakeCase.apply_to_variant(original), m_snake);
|
||||||
assert_eq!(MatrixDottedCase.apply_to_variant(original), m_dotted);
|
assert_eq!(MatrixDottedCase.apply_to_variant(original), m_dotted);
|
||||||
assert_eq!(MatrixRuleSnakeCase.apply_to_variant(original), m_rule_snake);
|
assert_eq!(MatrixRuleSnakeCase.apply_to_variant(original), m_rule_snake);
|
||||||
@ -229,6 +254,7 @@ fn rename_fields() {
|
|||||||
kebab,
|
kebab,
|
||||||
screaming_kebab,
|
screaming_kebab,
|
||||||
matrix_error,
|
matrix_error,
|
||||||
|
m_lower,
|
||||||
m_snake,
|
m_snake,
|
||||||
m_dotted,
|
m_dotted,
|
||||||
m_rule_snake,
|
m_rule_snake,
|
||||||
@ -245,6 +271,7 @@ fn rename_fields() {
|
|||||||
"M_OUTCOME",
|
"M_OUTCOME",
|
||||||
"m.outcome",
|
"m.outcome",
|
||||||
"m.outcome",
|
"m.outcome",
|
||||||
|
"m.outcome",
|
||||||
".m.rule.outcome",
|
".m.rule.outcome",
|
||||||
"m.role.outcome",
|
"m.role.outcome",
|
||||||
),
|
),
|
||||||
@ -258,11 +285,12 @@ fn rename_fields() {
|
|||||||
"VERY-TASTY",
|
"VERY-TASTY",
|
||||||
"M_VERY_TASTY",
|
"M_VERY_TASTY",
|
||||||
"m.very_tasty",
|
"m.very_tasty",
|
||||||
|
"m.very_tasty",
|
||||||
"m.very.tasty",
|
"m.very.tasty",
|
||||||
".m.rule.very_tasty",
|
".m.rule.very_tasty",
|
||||||
"m.role.very_tasty",
|
"m.role.very_tasty",
|
||||||
),
|
),
|
||||||
("a", "A", "A", "a", "A", "a", "A", "M_A", "m.a", "m.a", ".m.rule.a", "m.role.a"),
|
("a", "A", "A", "a", "A", "a", "A", "M_A", "m.a", "m.a", "m.a", ".m.rule.a", "m.role.a"),
|
||||||
(
|
(
|
||||||
"z42",
|
"z42",
|
||||||
"Z42",
|
"Z42",
|
||||||
@ -274,6 +302,7 @@ fn rename_fields() {
|
|||||||
"M_Z42",
|
"M_Z42",
|
||||||
"m.z42",
|
"m.z42",
|
||||||
"m.z42",
|
"m.z42",
|
||||||
|
"m.z42",
|
||||||
".m.rule.z42",
|
".m.rule.z42",
|
||||||
"m.role.z42",
|
"m.role.z42",
|
||||||
),
|
),
|
||||||
@ -287,6 +316,7 @@ fn rename_fields() {
|
|||||||
assert_eq!(KebabCase.apply_to_field(original), kebab);
|
assert_eq!(KebabCase.apply_to_field(original), kebab);
|
||||||
assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
|
assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
|
||||||
assert_eq!(MatrixErrorCase.apply_to_field(original), matrix_error);
|
assert_eq!(MatrixErrorCase.apply_to_field(original), matrix_error);
|
||||||
|
assert_eq!(MatrixLowerCase.apply_to_field(original), m_lower);
|
||||||
assert_eq!(MatrixSnakeCase.apply_to_field(original), m_snake);
|
assert_eq!(MatrixSnakeCase.apply_to_field(original), m_snake);
|
||||||
assert_eq!(MatrixDottedCase.apply_to_field(original), m_dotted);
|
assert_eq!(MatrixDottedCase.apply_to_field(original), m_dotted);
|
||||||
assert_eq!(MatrixRuleSnakeCase.apply_to_field(original), m_rule_snake);
|
assert_eq!(MatrixRuleSnakeCase.apply_to_field(original), m_rule_snake);
|
||||||
|
@ -269,6 +269,7 @@ unstable-msc3955 = ["ruma-events?/unstable-msc3955"]
|
|||||||
unstable-msc3956 = ["ruma-events?/unstable-msc3956"]
|
unstable-msc3956 = ["ruma-events?/unstable-msc3956"]
|
||||||
unstable-msc3983 = ["ruma-client-api?/unstable-msc3983"]
|
unstable-msc3983 = ["ruma-client-api?/unstable-msc3983"]
|
||||||
unstable-msc4075 = ["ruma-events?/unstable-msc4075"]
|
unstable-msc4075 = ["ruma-events?/unstable-msc4075"]
|
||||||
|
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
|
||||||
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
||||||
unstable-unspecified = [
|
unstable-unspecified = [
|
||||||
"ruma-common/unstable-unspecified",
|
"ruma-common/unstable-unspecified",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user