Merge remote-tracking branch 'upstream/main' into conduwuit-changes
This commit is contained in:
commit
042444dc1d
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -4,7 +4,7 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||||
# Keep in sync with version in `rust-toolchain.toml` and `xtask/src/ci.rs`
|
# Keep in sync with version in `rust-toolchain.toml` and `xtask/src/ci.rs`
|
||||||
NIGHTLY: nightly-2024-02-14
|
NIGHTLY: nightly-2024-05-09
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -20,7 +20,7 @@ ruma-appservice-api = { version = "0.10.0", path = "crates/ruma-appservice-api"
|
|||||||
ruma-common = { version = "0.13.0", path = "crates/ruma-common" }
|
ruma-common = { version = "0.13.0", path = "crates/ruma-common" }
|
||||||
ruma-client = { version = "0.13.0", path = "crates/ruma-client" }
|
ruma-client = { version = "0.13.0", path = "crates/ruma-client" }
|
||||||
ruma-client-api = { version = "0.18.0", path = "crates/ruma-client-api" }
|
ruma-client-api = { version = "0.18.0", path = "crates/ruma-client-api" }
|
||||||
ruma-events = { version = "0.28.0", path = "crates/ruma-events" }
|
ruma-events = { version = "0.28.1", path = "crates/ruma-events" }
|
||||||
ruma-federation-api = { version = "0.9.0", path = "crates/ruma-federation-api" }
|
ruma-federation-api = { version = "0.9.0", path = "crates/ruma-federation-api" }
|
||||||
ruma-html = { version = "0.2.0", path = "crates/ruma-html" }
|
ruma-html = { version = "0.2.0", path = "crates/ruma-html" }
|
||||||
ruma-identifiers-validation = { version = "0.9.5", path = "crates/ruma-identifiers-validation" }
|
ruma-identifiers-validation = { version = "0.9.5", path = "crates/ruma-identifiers-validation" }
|
||||||
@ -35,6 +35,7 @@ serde_html_form = "0.2.0"
|
|||||||
serde_json = "1.0.87"
|
serde_json = "1.0.87"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
tracing = { version = "0.1.37", default-features = false, features = ["std"] }
|
tracing = { version = "0.1.37", default-features = false, features = ["std"] }
|
||||||
|
url = { version = "2.5.0" }
|
||||||
web-time = "1.1.0"
|
web-time = "1.1.0"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
- Add support for MSC4108 OIDC sign in and E2EE set up via QR code
|
||||||
|
|
||||||
# 0.18.0
|
# 0.18.0
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
@ -50,6 +50,7 @@ unstable-msc3575 = []
|
|||||||
unstable-msc3814 = []
|
unstable-msc3814 = []
|
||||||
unstable-msc3843 = []
|
unstable-msc3843 = []
|
||||||
unstable-msc3983 = []
|
unstable-msc3983 = []
|
||||||
|
unstable-msc4108 = []
|
||||||
unstable-msc4121 = []
|
unstable-msc4121 = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -67,6 +68,7 @@ serde = { workspace = true }
|
|||||||
serde_html_form = { workspace = true }
|
serde_html_form = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
url = { workspace = true, features = ["serde"] }
|
||||||
web-time = { workspace = true }
|
web-time = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -22,8 +22,7 @@ pub fn system_time_to_http_date(
|
|||||||
date_header::format(duration.as_secs(), &mut buffer)
|
date_header::format(duration.as_secs(), &mut buffer)
|
||||||
.map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
|
.map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
|
||||||
|
|
||||||
Ok(http::HeaderValue::from_bytes(&buffer)
|
Ok(HeaderValue::from_bytes(&buffer).expect("date_header should produce a valid header value"))
|
||||||
.expect("date_header should produce a valid header value"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a header value representing a HTTP date to a `SystemTime`.
|
/// Convert a header value representing a HTTP date to a `SystemTime`.
|
||||||
|
@ -35,6 +35,8 @@ pub mod read_marker;
|
|||||||
pub mod receipt;
|
pub mod receipt;
|
||||||
pub mod redact;
|
pub mod redact;
|
||||||
pub mod relations;
|
pub mod relations;
|
||||||
|
#[cfg(feature = "unstable-msc4108")]
|
||||||
|
pub mod rendezvous;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
@ -148,11 +148,11 @@ pub mod v3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (scope, kind, rule_id): (RuleScope, RuleKind, String) =
|
let (scope, kind, rule_id): (RuleScope, RuleKind, String) =
|
||||||
serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
|
Deserialize::deserialize(serde::de::value::SeqDeserializer::<
|
||||||
_,
|
_,
|
||||||
serde::de::value::Error,
|
serde::de::value::Error,
|
||||||
>::new(
|
>::new(
|
||||||
path_args.iter().map(::std::convert::AsRef::as_ref),
|
path_args.iter().map(::std::convert::AsRef::as_ref)
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
let IncomingRequestQuery { before, after } =
|
let IncomingRequestQuery { before, after } =
|
||||||
|
3
crates/ruma-client-api/src/rendezvous.rs
Normal file
3
crates/ruma-client-api/src/rendezvous.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
//! Endpoints for managing rendezvous sessions.
|
||||||
|
|
||||||
|
pub mod create_rendezvous_session;
|
@ -0,0 +1,207 @@
|
|||||||
|
//! `POST /_matrix/client/*/rendezvous/`
|
||||||
|
//!
|
||||||
|
//! Create a rendezvous session.
|
||||||
|
|
||||||
|
pub mod unstable {
|
||||||
|
//! `msc4108` ([MSC])
|
||||||
|
//!
|
||||||
|
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||||
|
|
||||||
|
use http::{
|
||||||
|
header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, EXPIRES, LAST_MODIFIED},
|
||||||
|
HeaderName,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
use ruma_common::api::error::FromHttpResponseError;
|
||||||
|
use ruma_common::{
|
||||||
|
api::{error::HeaderDeserializationError, Metadata},
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use web_time::SystemTime;
|
||||||
|
|
||||||
|
const METADATA: Metadata = metadata! {
|
||||||
|
method: POST,
|
||||||
|
rate_limited: true,
|
||||||
|
authentication: None,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/client/unstable/org.matrix.msc4108/rendezvous",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Request type for the `POST` `rendezvous` endpoint.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct Request {
|
||||||
|
/// Any data up to maximum size allowed by the server.
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
impl ruma_common::api::OutgoingRequest for Request {
|
||||||
|
type EndpointError = crate::Error;
|
||||||
|
type IncomingResponse = Response;
|
||||||
|
const METADATA: Metadata = METADATA;
|
||||||
|
|
||||||
|
fn try_into_http_request<T: Default + bytes::BufMut>(
|
||||||
|
self,
|
||||||
|
base_url: &str,
|
||||||
|
_: ruma_common::api::SendAccessToken<'_>,
|
||||||
|
considering_versions: &'_ [ruma_common::api::MatrixVersion],
|
||||||
|
) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
|
||||||
|
let url = METADATA.make_endpoint_url(considering_versions, base_url, &[], "")?;
|
||||||
|
let body = self.content.as_bytes();
|
||||||
|
let content_length = body.len();
|
||||||
|
|
||||||
|
Ok(http::Request::builder()
|
||||||
|
.method(METADATA.method)
|
||||||
|
.uri(url)
|
||||||
|
.header(CONTENT_TYPE, "text/plain")
|
||||||
|
.header(CONTENT_LENGTH, content_length)
|
||||||
|
.body(ruma_common::serde::slice_to_buf(body))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
impl ruma_common::api::IncomingRequest for Request {
|
||||||
|
type EndpointError = crate::Error;
|
||||||
|
type OutgoingResponse = Response;
|
||||||
|
const METADATA: Metadata = METADATA;
|
||||||
|
|
||||||
|
fn try_from_http_request<B, S>(
|
||||||
|
request: http::Request<B>,
|
||||||
|
_path_args: &[S],
|
||||||
|
) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
|
||||||
|
where
|
||||||
|
B: AsRef<[u8]>,
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
const EXPECTED_CONTENT_TYPE: &str = "text/plain";
|
||||||
|
|
||||||
|
use ruma_common::api::error::DeserializationError;
|
||||||
|
|
||||||
|
let content_type = request
|
||||||
|
.headers()
|
||||||
|
.get(CONTENT_TYPE)
|
||||||
|
.ok_or(HeaderDeserializationError::MissingHeader(CONTENT_TYPE.to_string()))?;
|
||||||
|
|
||||||
|
let content_type = content_type.to_str()?;
|
||||||
|
|
||||||
|
if content_type != EXPECTED_CONTENT_TYPE {
|
||||||
|
Err(HeaderDeserializationError::InvalidHeaderValue {
|
||||||
|
header: CONTENT_TYPE.to_string(),
|
||||||
|
expected: EXPECTED_CONTENT_TYPE.to_owned(),
|
||||||
|
unexpected: content_type.to_owned(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
} else {
|
||||||
|
let body = request.into_body().as_ref().to_vec();
|
||||||
|
let content = String::from_utf8(body)
|
||||||
|
.map_err(|e| DeserializationError::Utf8(e.utf8_error()))?;
|
||||||
|
|
||||||
|
Ok(Self { content })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Creates a new `Request` with the given content.
|
||||||
|
pub fn new(content: String) -> Self {
|
||||||
|
Self { content }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the `POST` `rendezvous` endpoint.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct Response {
|
||||||
|
/// The absolute URL of the rendezvous session.
|
||||||
|
pub url: Url,
|
||||||
|
|
||||||
|
/// ETag for the current payload at the rendezvous session as
|
||||||
|
/// per [RFC7232](https://httpwg.org/specs/rfc7232.html#header.etag).
|
||||||
|
pub etag: String,
|
||||||
|
|
||||||
|
/// The expiry time of the rendezvous as per
|
||||||
|
/// [RFC7234](https://httpwg.org/specs/rfc7234.html#header.expires).
|
||||||
|
pub expires: SystemTime,
|
||||||
|
|
||||||
|
/// The last modified date of the payload as
|
||||||
|
/// per [RFC7232](https://httpwg.org/specs/rfc7232.html#header.last-modified)
|
||||||
|
pub last_modified: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct ResponseBody {
|
||||||
|
url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
impl ruma_common::api::IncomingResponse for Response {
|
||||||
|
type EndpointError = crate::Error;
|
||||||
|
|
||||||
|
fn try_from_http_response<T: AsRef<[u8]>>(
|
||||||
|
response: http::Response<T>,
|
||||||
|
) -> Result<Self, FromHttpResponseError<Self::EndpointError>> {
|
||||||
|
use ruma_common::api::EndpointError;
|
||||||
|
|
||||||
|
if response.status().as_u16() >= 400 {
|
||||||
|
return Err(FromHttpResponseError::Server(
|
||||||
|
Self::EndpointError::from_http_response(response),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let get_date = |header: HeaderName| -> Result<SystemTime, FromHttpResponseError<Self::EndpointError>> {
|
||||||
|
let date = response
|
||||||
|
.headers()
|
||||||
|
.get(&header)
|
||||||
|
.ok_or_else(|| HeaderDeserializationError::MissingHeader(header.to_string()))?;
|
||||||
|
|
||||||
|
let date = crate::http_headers::http_date_to_system_time(date)?;
|
||||||
|
|
||||||
|
Ok(date)
|
||||||
|
};
|
||||||
|
|
||||||
|
let etag = response
|
||||||
|
.headers()
|
||||||
|
.get(ETAG)
|
||||||
|
.ok_or(HeaderDeserializationError::MissingHeader(ETAG.to_string()))?
|
||||||
|
.to_str()?
|
||||||
|
.to_owned();
|
||||||
|
let expires = get_date(EXPIRES)?;
|
||||||
|
let last_modified = get_date(LAST_MODIFIED)?;
|
||||||
|
|
||||||
|
let body = response.into_body();
|
||||||
|
let body: ResponseBody = serde_json::from_slice(body.as_ref())?;
|
||||||
|
|
||||||
|
Ok(Self { url: body.url, etag, expires, last_modified })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
impl ruma_common::api::OutgoingResponse for Response {
|
||||||
|
fn try_into_http_response<T: Default + bytes::BufMut>(
|
||||||
|
self,
|
||||||
|
) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> {
|
||||||
|
use http::header::{CACHE_CONTROL, PRAGMA};
|
||||||
|
|
||||||
|
let body = ResponseBody { url: self.url.clone() };
|
||||||
|
let body = serde_json::to_vec(&body)?;
|
||||||
|
let body = ruma_common::serde::slice_to_buf(&body);
|
||||||
|
|
||||||
|
let expires = crate::http_headers::system_time_to_http_date(&self.expires)?;
|
||||||
|
let last_modified = crate::http_headers::system_time_to_http_date(&self.last_modified)?;
|
||||||
|
|
||||||
|
Ok(http::Response::builder()
|
||||||
|
.status(http::StatusCode::OK)
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
|
.header(PRAGMA, "no-cache")
|
||||||
|
.header(CACHE_CONTROL, "no-store")
|
||||||
|
.header(ETAG, self.etag)
|
||||||
|
.header(EXPIRES, expires)
|
||||||
|
.header(LAST_MODIFIED, last_modified)
|
||||||
|
.body(body)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -145,7 +145,7 @@ impl<C: HttpClient> Client<C> {
|
|||||||
.send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }))
|
.send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
*self.0.access_token.lock().unwrap() = response.access_token.clone();
|
self.0.access_token.lock().unwrap().clone_from(&response.access_token);
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ impl<C: HttpClient> Client<C> {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
*self.0.access_token.lock().unwrap() = response.access_token.clone();
|
self.0.access_token.lock().unwrap().clone_from(&response.access_token);
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
@ -221,7 +221,7 @@ impl<C: HttpClient> Client<C> {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
since = response.next_batch.clone();
|
since.clone_from(&response.next_batch);
|
||||||
yield response;
|
yield response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
- Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for
|
||||||
|
cases where we receive a HTTP header with an unexpected value.
|
||||||
|
|
||||||
# 0.13.0
|
# 0.13.0
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
@ -85,7 +85,7 @@ serde_json = { workspace = true, features = ["raw_value"] }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
time = "0.3.34"
|
time = "0.3.34"
|
||||||
tracing = { workspace = true, features = ["attributes"] }
|
tracing = { workspace = true, features = ["attributes"] }
|
||||||
url = "2.2.2"
|
url = { workspace = true }
|
||||||
uuid = { version = "1.0.0", optional = true, features = ["v4"] }
|
uuid = { version = "1.0.0", optional = true, features = ["v4"] }
|
||||||
web-time = { workspace = true }
|
web-time = { workspace = true }
|
||||||
wildmatch = "2.0.0"
|
wildmatch = "2.0.0"
|
||||||
|
@ -276,6 +276,20 @@ pub enum HeaderDeserializationError {
|
|||||||
/// The given required header is missing.
|
/// The given required header is missing.
|
||||||
#[error("missing header `{0}`")]
|
#[error("missing header `{0}`")]
|
||||||
MissingHeader(String),
|
MissingHeader(String),
|
||||||
|
|
||||||
|
/// A header was received with a unexpected value.
|
||||||
|
#[error(
|
||||||
|
"The {header} header was received with an unexpected value, \
|
||||||
|
expected {expected}, received {unexpected}"
|
||||||
|
)]
|
||||||
|
InvalidHeaderValue {
|
||||||
|
/// The name of the header containing the invalid value.
|
||||||
|
header: String,
|
||||||
|
/// The value the header should have been set to.
|
||||||
|
expected: String,
|
||||||
|
/// The value we instead received and rejected.
|
||||||
|
unexpected: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that happens when Ruma cannot understand a Matrix version.
|
/// An error that happens when Ruma cannot understand a Matrix version.
|
||||||
|
@ -58,10 +58,7 @@ impl<'de> Visitor<'de> for RoomNetworkVisitor {
|
|||||||
while let Some((key, value)) = access.next_entry::<String, JsonValue>()? {
|
while let Some((key, value)) = access.next_entry::<String, JsonValue>()? {
|
||||||
match key.as_str() {
|
match key.as_str() {
|
||||||
"include_all_networks" => {
|
"include_all_networks" => {
|
||||||
include_all_networks = match value.as_bool() {
|
include_all_networks = value.as_bool().unwrap_or(false);
|
||||||
Some(b) => b,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"third_party_instance_id" => {
|
"third_party_instance_id" => {
|
||||||
third_party_instance_id = value.as_str().map(|v| v.to_owned());
|
third_party_instance_id = value.as_str().map(|v| v.to_owned());
|
||||||
|
@ -153,7 +153,7 @@ impl<T> Raw<T> {
|
|||||||
{
|
{
|
||||||
type Value = Option<T>;
|
type Value = Option<T>;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
formatter.write_str("a string")
|
formatter.write_str("a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ impl IncomingRequest for Request {
|
|||||||
B: AsRef<[u8]>,
|
B: AsRef<[u8]>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
let (room_alias,) = serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
|
let (room_alias,) = Deserialize::deserialize(serde::de::value::SeqDeserializer::<
|
||||||
_,
|
_,
|
||||||
serde::de::value::Error,
|
serde::de::value::Error,
|
||||||
>::new(
|
>::new(
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
# 0.28.1
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
- Implement `make_for_thread` and `make_replacement` for
|
||||||
|
`RoomMessageEventContentWithoutRelation`
|
||||||
|
- `RoomMessageEventContent::set_mentions` is deprecated and replaced by
|
||||||
|
`add_mentions` that should be called before `make_replacement`.
|
||||||
|
|
||||||
# 0.28.0
|
# 0.28.0
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ruma-events"
|
name = "ruma-events"
|
||||||
version = "0.28.0"
|
version = "0.28.1"
|
||||||
description = "Serializable types for the events in the Matrix specification."
|
description = "Serializable types for the events in the Matrix specification."
|
||||||
homepage = "https://ruma.dev/"
|
homepage = "https://ruma.dev/"
|
||||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||||
@ -72,7 +72,7 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true, features = ["attributes"] }
|
tracing = { workspace = true, features = ["attributes"] }
|
||||||
url = "2.2.2"
|
url = { workspace = true }
|
||||||
wildmatch = "2.0.0"
|
wildmatch = "2.0.0"
|
||||||
|
|
||||||
# dev-dependencies can't be optional, so this is a regular dependency
|
# dev-dependencies can't be optional, so this is a regular dependency
|
||||||
|
@ -141,7 +141,7 @@ impl<'de> Deserialize<'de> for JoinRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
|
let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
|
||||||
.map_err(serde::de::Error::custom)?
|
.map_err(Error::custom)?
|
||||||
.join_rule
|
.join_rule
|
||||||
.ok_or_else(|| D::Error::missing_field("join_rule"))?;
|
.ok_or_else(|| D::Error::missing_field("join_rule"))?;
|
||||||
|
|
||||||
@ -238,9 +238,8 @@ impl<'de> Deserialize<'de> for AllowRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the value of `type` if present.
|
// Get the value of `type` if present.
|
||||||
let rule_type = serde_json::from_str::<ExtractType<'_>>(json.get())
|
let rule_type =
|
||||||
.map_err(serde::de::Error::custom)?
|
serde_json::from_str::<ExtractType<'_>>(json.get()).map_err(Error::custom)?.rule_type;
|
||||||
.rule_type;
|
|
||||||
|
|
||||||
match rule_type.as_deref() {
|
match rule_type.as_deref() {
|
||||||
Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
|
Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
|
||||||
|
@ -218,30 +218,12 @@ impl RoomMessageEventContent {
|
|||||||
/// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
|
/// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
|
||||||
/// other than HTML.
|
/// other than HTML.
|
||||||
pub fn make_for_thread(
|
pub fn make_for_thread(
|
||||||
mut self,
|
self,
|
||||||
previous_message: &OriginalRoomMessageEvent,
|
previous_message: &OriginalRoomMessageEvent,
|
||||||
is_reply: ReplyWithinThread,
|
is_reply: ReplyWithinThread,
|
||||||
add_mentions: AddMentions,
|
add_mentions: AddMentions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
if is_reply == ReplyWithinThread::Yes {
|
self.without_relation().make_for_thread(previous_message, is_reply, add_mentions)
|
||||||
self = self.make_reply_to(previous_message, ForwardThread::No, add_mentions);
|
|
||||||
}
|
|
||||||
|
|
||||||
let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) =
|
|
||||||
&previous_message.content.relates_to
|
|
||||||
{
|
|
||||||
event_id.clone()
|
|
||||||
} else {
|
|
||||||
previous_message.event_id.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.relates_to = Some(Relation::Thread(Thread {
|
|
||||||
event_id: thread_root,
|
|
||||||
in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }),
|
|
||||||
is_falling_back: is_reply == ReplyWithinThread::No,
|
|
||||||
}));
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turns `self` into a [replacement] (or edit) for a given message.
|
/// Turns `self` into a [replacement] (or edit) for a given message.
|
||||||
@ -259,9 +241,9 @@ impl RoomMessageEventContent {
|
|||||||
/// `original_message`.
|
/// `original_message`.
|
||||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
|
||||||
///
|
///
|
||||||
/// If the message that is replaced contains [`Mentions`], they are copied into
|
/// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
|
||||||
/// `m.new_content` to keep the same mentions, but not into `content` to avoid repeated
|
/// mentions, but the ones in `content` are filtered with the ones in the
|
||||||
/// notifications.
|
/// [`ReplacementMetadata`] so only new mentions will trigger a notification.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
@ -270,31 +252,11 @@ impl RoomMessageEventContent {
|
|||||||
/// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
|
/// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn make_replacement(
|
pub fn make_replacement(
|
||||||
mut self,
|
self,
|
||||||
metadata: impl Into<ReplacementMetadata>,
|
metadata: impl Into<ReplacementMetadata>,
|
||||||
replied_to_message: Option<&OriginalRoomMessageEvent>,
|
replied_to_message: Option<&OriginalRoomMessageEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let metadata = metadata.into();
|
self.without_relation().make_replacement(metadata, replied_to_message)
|
||||||
|
|
||||||
// Prepare relates_to with the untouched msgtype.
|
|
||||||
let relates_to = Relation::Replacement(Replacement {
|
|
||||||
event_id: metadata.event_id,
|
|
||||||
new_content: RoomMessageEventContentWithoutRelation {
|
|
||||||
msgtype: self.msgtype.clone(),
|
|
||||||
mentions: metadata.mentions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
self.msgtype.make_replacement_body();
|
|
||||||
|
|
||||||
// Add reply fallback if needed.
|
|
||||||
if let Some(original_message) = replied_to_message {
|
|
||||||
self = self.make_reply_to(original_message, ForwardThread::No, AddMentions::No);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.relates_to = Some(relates_to);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the [mentions] of this event.
|
/// Set the [mentions] of this event.
|
||||||
@ -308,6 +270,7 @@ impl RoomMessageEventContent {
|
|||||||
/// used instead.
|
/// used instead.
|
||||||
///
|
///
|
||||||
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
|
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
|
||||||
|
#[deprecated = "Call add_mentions before adding the relation instead."]
|
||||||
pub fn set_mentions(mut self, mentions: Mentions) -> Self {
|
pub fn set_mentions(mut self, mentions: Mentions) -> Self {
|
||||||
if let Some(Relation::Replacement(replacement)) = &mut self.relates_to {
|
if let Some(Relation::Replacement(replacement)) = &mut self.relates_to {
|
||||||
let old_mentions = &replacement.new_content.mentions;
|
let old_mentions = &replacement.new_content.mentions;
|
||||||
@ -344,9 +307,8 @@ impl RoomMessageEventContent {
|
|||||||
/// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to
|
/// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to
|
||||||
/// the values of `room`.
|
/// the values of `room`.
|
||||||
///
|
///
|
||||||
/// This is recommended over [`Self::set_mentions()`] to avoid to overwrite any mentions set
|
/// This should be called before methods that add a relation, like [`Self::make_reply_to()`] and
|
||||||
/// automatically by another method, like [`Self::make_reply_to()`]. However, this method has no
|
/// [`Self::make_replacement()`], for the mentions to be correctly set.
|
||||||
/// special support for replacements.
|
|
||||||
///
|
///
|
||||||
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
|
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
|
||||||
pub fn add_mentions(mut self, mentions: Mentions) -> Self {
|
pub fn add_mentions(mut self, mentions: Mentions) -> Self {
|
||||||
@ -686,6 +648,8 @@ impl MessageType {
|
|||||||
if let Some(formatted) = formatted {
|
if let Some(formatted) = formatted {
|
||||||
formatted.sanitize_html(mode, remove_reply_fallback);
|
formatted.sanitize_html(mode, remove_reply_fallback);
|
||||||
}
|
}
|
||||||
|
// This is a false positive, see <https://github.com/rust-lang/rust-clippy/issues/12444>
|
||||||
|
#[allow(clippy::assigning_clones)]
|
||||||
if remove_reply_fallback == RemoveReplyFallback::Yes {
|
if remove_reply_fallback == RemoveReplyFallback::Yes {
|
||||||
*body = remove_plain_reply_fallback(body).to_owned();
|
*body = remove_plain_reply_fallback(body).to_owned();
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation,
|
AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation,
|
||||||
RoomMessageEventContent,
|
ReplacementMetadata, ReplyWithinThread, RoomMessageEventContent,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
relation::{InReplyTo, Thread},
|
relation::{InReplyTo, Replacement, Thread},
|
||||||
room::message::{reply::OriginalEventData, FormattedBody},
|
room::message::{reply::OriginalEventData, FormattedBody},
|
||||||
AnySyncTimelineEvent, Mentions,
|
AnySyncTimelineEvent, Mentions,
|
||||||
};
|
};
|
||||||
@ -209,6 +209,127 @@ impl RoomMessageEventContentWithoutRelation {
|
|||||||
self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
|
self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turns `self` into a new message for a thread, that is optionally a reply.
|
||||||
|
///
|
||||||
|
/// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, this message will be
|
||||||
|
/// in the same thread. If it doesn't, a new thread with `previous_message` as the root is
|
||||||
|
/// created.
|
||||||
|
///
|
||||||
|
/// If this is a reply within the thread, takes the `body` / `formatted_body` (if any) in `self`
|
||||||
|
/// for the main text and prepends a quoted version of `previous_message`. Also sets the
|
||||||
|
/// `in_reply_to` field inside `relates_to`.
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
|
||||||
|
/// other than HTML.
|
||||||
|
pub fn make_for_thread(
|
||||||
|
self,
|
||||||
|
previous_message: &OriginalRoomMessageEvent,
|
||||||
|
is_reply: ReplyWithinThread,
|
||||||
|
add_mentions: AddMentions,
|
||||||
|
) -> RoomMessageEventContent {
|
||||||
|
let mut content = if is_reply == ReplyWithinThread::Yes {
|
||||||
|
self.make_reply_to(previous_message, ForwardThread::No, add_mentions)
|
||||||
|
} else {
|
||||||
|
self.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) =
|
||||||
|
&previous_message.content.relates_to
|
||||||
|
{
|
||||||
|
event_id.clone()
|
||||||
|
} else {
|
||||||
|
previous_message.event_id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
content.relates_to = Some(Relation::Thread(Thread {
|
||||||
|
event_id: thread_root,
|
||||||
|
in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }),
|
||||||
|
is_falling_back: is_reply == ReplyWithinThread::No,
|
||||||
|
}));
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns `self` into a [replacement] (or edit) for a given message.
|
||||||
|
///
|
||||||
|
/// The first argument after `self` can be `&OriginalRoomMessageEvent` or
|
||||||
|
/// `&OriginalSyncRoomMessageEvent` if you don't want to create `ReplacementMetadata` separately
|
||||||
|
/// before calling this function.
|
||||||
|
///
|
||||||
|
/// This takes the content and sets it in `m.new_content`, and modifies the `content` to include
|
||||||
|
/// a fallback.
|
||||||
|
///
|
||||||
|
/// If the message that is replaced is a reply to another message, the latter should also be
|
||||||
|
/// provided to be able to generate a rich reply fallback that takes the `body` /
|
||||||
|
/// `formatted_body` (if any) in `self` for the main text and prepends a quoted version of
|
||||||
|
/// `original_message`.
|
||||||
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
|
||||||
|
///
|
||||||
|
/// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
|
||||||
|
/// mentions, but the ones in `content` are filtered with the ones in the
|
||||||
|
/// [`ReplacementMetadata`] so only new mentions will trigger a notification.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `self` has a `formatted_body` with a format other than HTML.
|
||||||
|
///
|
||||||
|
/// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
|
||||||
|
#[track_caller]
|
||||||
|
pub fn make_replacement(
|
||||||
|
mut self,
|
||||||
|
metadata: impl Into<ReplacementMetadata>,
|
||||||
|
replied_to_message: Option<&OriginalRoomMessageEvent>,
|
||||||
|
) -> RoomMessageEventContent {
|
||||||
|
let metadata = metadata.into();
|
||||||
|
|
||||||
|
let mentions = self.mentions.take();
|
||||||
|
|
||||||
|
// Only set mentions that were not there before.
|
||||||
|
if let Some(mentions) = &mentions {
|
||||||
|
let new_mentions = metadata.mentions.map(|old_mentions| {
|
||||||
|
let mut new_mentions = Mentions::new();
|
||||||
|
|
||||||
|
new_mentions.user_ids = mentions
|
||||||
|
.user_ids
|
||||||
|
.iter()
|
||||||
|
.filter(|u| !old_mentions.user_ids.contains(*u))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
new_mentions.room = mentions.room && !old_mentions.room;
|
||||||
|
|
||||||
|
new_mentions
|
||||||
|
});
|
||||||
|
|
||||||
|
self.mentions = new_mentions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare relates_to with the untouched msgtype.
|
||||||
|
let relates_to = Relation::Replacement(Replacement {
|
||||||
|
event_id: metadata.event_id,
|
||||||
|
new_content: RoomMessageEventContentWithoutRelation {
|
||||||
|
msgtype: self.msgtype.clone(),
|
||||||
|
mentions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
self.msgtype.make_replacement_body();
|
||||||
|
|
||||||
|
// Add reply fallback if needed.
|
||||||
|
let mut content = if let Some(original_message) = replied_to_message {
|
||||||
|
self.make_reply_to(original_message, ForwardThread::No, AddMentions::No)
|
||||||
|
} else {
|
||||||
|
self.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
content.relates_to = Some(relates_to);
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
/// Add the given [mentions] to this event.
|
/// Add the given [mentions] to this event.
|
||||||
///
|
///
|
||||||
/// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current
|
/// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current
|
||||||
@ -253,3 +374,10 @@ impl From<RoomMessageEventContent> for RoomMessageEventContentWithoutRelation {
|
|||||||
Self { msgtype, mentions }
|
Self { msgtype, mentions }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RoomMessageEventContentWithoutRelation> for RoomMessageEventContent {
|
||||||
|
fn from(value: RoomMessageEventContentWithoutRelation) -> Self {
|
||||||
|
let RoomMessageEventContentWithoutRelation { msgtype, mentions } = value;
|
||||||
|
Self { msgtype, relates_to: None, mentions }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1164,6 +1164,7 @@ fn video_msgtype_deserialization() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(deprecated)]
|
||||||
fn set_mentions() {
|
fn set_mentions() {
|
||||||
let mut content = RoomMessageEventContent::text_plain("you!");
|
let mut content = RoomMessageEventContent::text_plain("you!");
|
||||||
let mentions = content.mentions.take();
|
let mentions = content.mentions.take();
|
||||||
@ -1176,7 +1177,42 @@ fn set_mentions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn make_replacement_set_mentions() {
|
fn add_mentions_then_make_replacement() {
|
||||||
|
let alice = owned_user_id!("@alice:localhost");
|
||||||
|
let bob = owned_user_id!("@bob:localhost");
|
||||||
|
let original_message_json = json!({
|
||||||
|
"content": {
|
||||||
|
"body": "Hello, World!",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"m.mentions": {
|
||||||
|
"user_ids": [alice],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn",
|
||||||
|
"origin_server_ts": 134_829_848,
|
||||||
|
"room_id": "!roomid:notareal.hs",
|
||||||
|
"sender": "@user:notareal.hs",
|
||||||
|
"type": "m.room.message",
|
||||||
|
});
|
||||||
|
let original_message: OriginalSyncRoomMessageEvent =
|
||||||
|
from_json_value(original_message_json).unwrap();
|
||||||
|
|
||||||
|
let mut content = RoomMessageEventContent::text_html(
|
||||||
|
"This is _an edited_ message.",
|
||||||
|
"This is <em>an edited</em> message.",
|
||||||
|
);
|
||||||
|
content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
|
||||||
|
content = content.make_replacement(&original_message, None);
|
||||||
|
|
||||||
|
let mentions = content.mentions.unwrap();
|
||||||
|
assert_eq!(mentions.user_ids, [bob.clone()].into());
|
||||||
|
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
|
||||||
|
let mentions = replacement.new_content.mentions.unwrap();
|
||||||
|
assert_eq!(mentions.user_ids, [alice, bob].into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn make_replacement_then_add_mentions() {
|
||||||
let alice = owned_user_id!("@alice:localhost");
|
let alice = owned_user_id!("@alice:localhost");
|
||||||
let bob = owned_user_id!("@bob:localhost");
|
let bob = owned_user_id!("@bob:localhost");
|
||||||
let original_message_json = json!({
|
let original_message_json = json!({
|
||||||
@ -1201,19 +1237,12 @@ fn make_replacement_set_mentions() {
|
|||||||
"This is <em>an edited</em> message.",
|
"This is <em>an edited</em> message.",
|
||||||
);
|
);
|
||||||
content = content.make_replacement(&original_message, None);
|
content = content.make_replacement(&original_message, None);
|
||||||
let content_clone = content.clone();
|
content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
|
||||||
|
|
||||||
assert_matches!(content.mentions, None);
|
|
||||||
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
|
|
||||||
let mentions = replacement.new_content.mentions.unwrap();
|
|
||||||
assert_eq!(mentions.user_ids, [alice.clone()].into());
|
|
||||||
|
|
||||||
content = content_clone.set_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
|
|
||||||
let mentions = content.mentions.unwrap();
|
let mentions = content.mentions.unwrap();
|
||||||
assert_eq!(mentions.user_ids, [bob.clone()].into());
|
|
||||||
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
|
|
||||||
let mentions = replacement.new_content.mentions.unwrap();
|
|
||||||
assert_eq!(mentions.user_ids, [alice, bob].into());
|
assert_eq!(mentions.user_ids, [alice, bob].into());
|
||||||
|
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
|
||||||
|
assert!(replacement.new_content.mentions.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -6,13 +6,10 @@ use once_cell::sync::Lazy;
|
|||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use serde::{de::IgnoredAny, Deserialize};
|
use serde::{de::IgnoredAny, Deserialize};
|
||||||
|
|
||||||
mod api_metadata;
|
|
||||||
mod attribute;
|
mod attribute;
|
||||||
mod auth_scheme;
|
mod auth_scheme;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
mod util;
|
|
||||||
mod version;
|
|
||||||
|
|
||||||
mod kw {
|
mod kw {
|
||||||
syn::custom_keyword!(error);
|
syn::custom_keyword!(error);
|
||||||
|
@ -1,419 +0,0 @@
|
|||||||
//! Details of the `metadata` section of the procedural macro.
|
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::{quote, ToTokens};
|
|
||||||
use syn::{
|
|
||||||
braced,
|
|
||||||
parse::{Parse, ParseStream},
|
|
||||||
Ident, LitBool, LitStr, Token,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{auth_scheme::AuthScheme, util, version::MatrixVersionLiteral};
|
|
||||||
|
|
||||||
mod kw {
|
|
||||||
syn::custom_keyword!(metadata);
|
|
||||||
syn::custom_keyword!(description);
|
|
||||||
syn::custom_keyword!(method);
|
|
||||||
syn::custom_keyword!(name);
|
|
||||||
syn::custom_keyword!(unstable_path);
|
|
||||||
syn::custom_keyword!(r0_path);
|
|
||||||
syn::custom_keyword!(stable_path);
|
|
||||||
syn::custom_keyword!(rate_limited);
|
|
||||||
syn::custom_keyword!(authentication);
|
|
||||||
syn::custom_keyword!(added);
|
|
||||||
syn::custom_keyword!(deprecated);
|
|
||||||
syn::custom_keyword!(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of processing the `metadata` section of the macro.
|
|
||||||
pub struct Metadata {
|
|
||||||
/// The description field.
|
|
||||||
pub description: LitStr,
|
|
||||||
|
|
||||||
/// The method field.
|
|
||||||
pub method: Ident,
|
|
||||||
|
|
||||||
/// The name field.
|
|
||||||
pub name: LitStr,
|
|
||||||
|
|
||||||
/// The rate_limited field.
|
|
||||||
pub rate_limited: LitBool,
|
|
||||||
|
|
||||||
/// The authentication field.
|
|
||||||
pub authentication: AuthScheme,
|
|
||||||
|
|
||||||
/// The version history field.
|
|
||||||
pub history: History,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_field<T: ToTokens>(field: &mut Option<T>, value: T) -> syn::Result<()> {
|
|
||||||
match field {
|
|
||||||
Some(existing_value) => {
|
|
||||||
let mut error = syn::Error::new_spanned(value, "duplicate field assignment");
|
|
||||||
error.combine(syn::Error::new_spanned(existing_value, "first one here"));
|
|
||||||
Err(error)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
*field = Some(value);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for Metadata {
|
|
||||||
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
||||||
let metadata_kw: kw::metadata = input.parse()?;
|
|
||||||
let _: Token![:] = input.parse()?;
|
|
||||||
|
|
||||||
let field_values;
|
|
||||||
braced!(field_values in input);
|
|
||||||
|
|
||||||
let field_values = field_values.parse_terminated(FieldValue::parse, Token![,])?;
|
|
||||||
|
|
||||||
let mut description = None;
|
|
||||||
let mut method = None;
|
|
||||||
let mut name = None;
|
|
||||||
let mut unstable_path = None;
|
|
||||||
let mut r0_path = None;
|
|
||||||
let mut stable_path = None;
|
|
||||||
let mut rate_limited = None;
|
|
||||||
let mut authentication = None;
|
|
||||||
let mut added = None;
|
|
||||||
let mut deprecated = None;
|
|
||||||
let mut removed = None;
|
|
||||||
|
|
||||||
for field_value in field_values {
|
|
||||||
match field_value {
|
|
||||||
FieldValue::Description(d) => set_field(&mut description, d)?,
|
|
||||||
FieldValue::Method(m) => set_field(&mut method, m)?,
|
|
||||||
FieldValue::Name(n) => set_field(&mut name, n)?,
|
|
||||||
FieldValue::UnstablePath(p) => set_field(&mut unstable_path, p)?,
|
|
||||||
FieldValue::R0Path(p) => set_field(&mut r0_path, p)?,
|
|
||||||
FieldValue::StablePath(p) => set_field(&mut stable_path, p)?,
|
|
||||||
FieldValue::RateLimited(rl) => set_field(&mut rate_limited, rl)?,
|
|
||||||
FieldValue::Authentication(a) => set_field(&mut authentication, a)?,
|
|
||||||
FieldValue::Added(v) => set_field(&mut added, v)?,
|
|
||||||
FieldValue::Deprecated(v) => set_field(&mut deprecated, v)?,
|
|
||||||
FieldValue::Removed(v) => set_field(&mut removed, v)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let missing_field =
|
|
||||||
|name| syn::Error::new_spanned(metadata_kw, format!("missing field `{name}`"));
|
|
||||||
|
|
||||||
// Construct the History object.
|
|
||||||
let history = {
|
|
||||||
let stable_or_r0 = stable_path.as_ref().or(r0_path.as_ref());
|
|
||||||
|
|
||||||
if let Some(path) = stable_or_r0 {
|
|
||||||
if added.is_none() {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
path,
|
|
||||||
"stable path was defined, while `added` version was not defined",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(deprecated) = &deprecated {
|
|
||||||
if added.is_none() {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
deprecated,
|
|
||||||
"deprecated version is defined while added version is not defined",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: It is possible that Matrix will remove endpoints in a single version, while
|
|
||||||
// not having a deprecation version inbetween, but that would not be allowed by their
|
|
||||||
// own deprecation policy, so lets just assume there's always a deprecation version
|
|
||||||
// before a removal one.
|
|
||||||
//
|
|
||||||
// If Matrix does so anyways, we can just alter this.
|
|
||||||
if let Some(removed) = &removed {
|
|
||||||
if deprecated.is_none() {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
removed,
|
|
||||||
"removed version is defined while deprecated version is not defined",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(added) = &added {
|
|
||||||
if stable_or_r0.is_none() {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
added,
|
|
||||||
"added version is defined, but no stable or r0 path exists",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(r0) = &r0_path {
|
|
||||||
let added =
|
|
||||||
added.as_ref().expect("we error if r0 or stable is defined without added");
|
|
||||||
|
|
||||||
if added.major.get() == 1 && added.minor > 0 {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
r0,
|
|
||||||
"r0 defined while added version is newer than v1.0",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if stable_path.is_none() {
|
|
||||||
return Err(syn::Error::new_spanned(r0, "r0 defined without stable path"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !r0.value().contains("/r0/") {
|
|
||||||
return Err(syn::Error::new_spanned(r0, "r0 endpoint does not contain /r0/"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(stable) = &stable_path {
|
|
||||||
if stable.value().contains("/r0/") {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
stable,
|
|
||||||
"stable endpoint contains /r0/ (did you make a copy-paste error?)",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if unstable_path.is_none() && r0_path.is_none() && stable_path.is_none() {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
metadata_kw,
|
|
||||||
"need to define one of [r0_path, stable_path, unstable_path]",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
History::construct(deprecated, removed, unstable_path, r0_path, stable_path.zip(added))
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
description: description.ok_or_else(|| missing_field("description"))?,
|
|
||||||
method: method.ok_or_else(|| missing_field("method"))?,
|
|
||||||
name: name.ok_or_else(|| missing_field("name"))?,
|
|
||||||
rate_limited: rate_limited.ok_or_else(|| missing_field("rate_limited"))?,
|
|
||||||
authentication: authentication.ok_or_else(|| missing_field("authentication"))?,
|
|
||||||
history,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Field {
|
|
||||||
Description,
|
|
||||||
Method,
|
|
||||||
Name,
|
|
||||||
UnstablePath,
|
|
||||||
R0Path,
|
|
||||||
StablePath,
|
|
||||||
RateLimited,
|
|
||||||
Authentication,
|
|
||||||
Added,
|
|
||||||
Deprecated,
|
|
||||||
Removed,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for Field {
|
|
||||||
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
||||||
let lookahead = input.lookahead1();
|
|
||||||
|
|
||||||
if lookahead.peek(kw::description) {
|
|
||||||
let _: kw::description = input.parse()?;
|
|
||||||
Ok(Self::Description)
|
|
||||||
} else if lookahead.peek(kw::method) {
|
|
||||||
let _: kw::method = input.parse()?;
|
|
||||||
Ok(Self::Method)
|
|
||||||
} else if lookahead.peek(kw::name) {
|
|
||||||
let _: kw::name = input.parse()?;
|
|
||||||
Ok(Self::Name)
|
|
||||||
} else if lookahead.peek(kw::unstable_path) {
|
|
||||||
let _: kw::unstable_path = input.parse()?;
|
|
||||||
Ok(Self::UnstablePath)
|
|
||||||
} else if lookahead.peek(kw::r0_path) {
|
|
||||||
let _: kw::r0_path = input.parse()?;
|
|
||||||
Ok(Self::R0Path)
|
|
||||||
} else if lookahead.peek(kw::stable_path) {
|
|
||||||
let _: kw::stable_path = input.parse()?;
|
|
||||||
Ok(Self::StablePath)
|
|
||||||
} else if lookahead.peek(kw::rate_limited) {
|
|
||||||
let _: kw::rate_limited = input.parse()?;
|
|
||||||
Ok(Self::RateLimited)
|
|
||||||
} else if lookahead.peek(kw::authentication) {
|
|
||||||
let _: kw::authentication = input.parse()?;
|
|
||||||
Ok(Self::Authentication)
|
|
||||||
} else if lookahead.peek(kw::added) {
|
|
||||||
let _: kw::added = input.parse()?;
|
|
||||||
Ok(Self::Added)
|
|
||||||
} else if lookahead.peek(kw::deprecated) {
|
|
||||||
let _: kw::deprecated = input.parse()?;
|
|
||||||
Ok(Self::Deprecated)
|
|
||||||
} else if lookahead.peek(kw::removed) {
|
|
||||||
let _: kw::removed = input.parse()?;
|
|
||||||
Ok(Self::Removed)
|
|
||||||
} else {
|
|
||||||
Err(lookahead.error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FieldValue {
|
|
||||||
Description(LitStr),
|
|
||||||
Method(Ident),
|
|
||||||
Name(LitStr),
|
|
||||||
UnstablePath(EndpointPath),
|
|
||||||
R0Path(EndpointPath),
|
|
||||||
StablePath(EndpointPath),
|
|
||||||
RateLimited(LitBool),
|
|
||||||
Authentication(AuthScheme),
|
|
||||||
Added(MatrixVersionLiteral),
|
|
||||||
Deprecated(MatrixVersionLiteral),
|
|
||||||
Removed(MatrixVersionLiteral),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for FieldValue {
|
|
||||||
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
||||||
let field: Field = input.parse()?;
|
|
||||||
let _: Token![:] = input.parse()?;
|
|
||||||
|
|
||||||
Ok(match field {
|
|
||||||
Field::Description => Self::Description(input.parse()?),
|
|
||||||
Field::Method => Self::Method(input.parse()?),
|
|
||||||
Field::Name => Self::Name(input.parse()?),
|
|
||||||
Field::UnstablePath => Self::UnstablePath(input.parse()?),
|
|
||||||
Field::R0Path => Self::R0Path(input.parse()?),
|
|
||||||
Field::StablePath => Self::StablePath(input.parse()?),
|
|
||||||
Field::RateLimited => Self::RateLimited(input.parse()?),
|
|
||||||
Field::Authentication => Self::Authentication(input.parse()?),
|
|
||||||
Field::Added => Self::Added(input.parse()?),
|
|
||||||
Field::Deprecated => Self::Deprecated(input.parse()?),
|
|
||||||
Field::Removed => Self::Removed(input.parse()?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub struct History {
|
|
||||||
pub(super) entries: Vec<HistoryEntry>,
|
|
||||||
misc: MiscVersioning,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl History {
|
|
||||||
// TODO(j0j0): remove after codebase conversion is complete
|
|
||||||
/// Construct a History object from legacy parts.
|
|
||||||
pub fn construct(
|
|
||||||
deprecated: Option<MatrixVersionLiteral>,
|
|
||||||
removed: Option<MatrixVersionLiteral>,
|
|
||||||
unstable_path: Option<EndpointPath>,
|
|
||||||
r0_path: Option<EndpointPath>,
|
|
||||||
stable_path_and_version: Option<(EndpointPath, MatrixVersionLiteral)>,
|
|
||||||
) -> Self {
|
|
||||||
// Unfortunately can't `use` associated constants
|
|
||||||
const V1_0: MatrixVersionLiteral = MatrixVersionLiteral::V1_0;
|
|
||||||
|
|
||||||
let unstable = unstable_path.map(|path| HistoryEntry::Unstable { path });
|
|
||||||
let r0 = r0_path.map(|path| HistoryEntry::Stable { path, version: V1_0 });
|
|
||||||
let stable = stable_path_and_version.map(|(path, mut version)| {
|
|
||||||
// If added in 1.0 as r0, the new stable path must be from 1.1
|
|
||||||
if r0.is_some() && version == V1_0 {
|
|
||||||
version = MatrixVersionLiteral::V1_1;
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryEntry::Stable { path, version }
|
|
||||||
});
|
|
||||||
|
|
||||||
let misc = match (deprecated, removed) {
|
|
||||||
(None, None) => MiscVersioning::None,
|
|
||||||
(Some(deprecated), None) => MiscVersioning::Deprecated(deprecated),
|
|
||||||
(Some(deprecated), Some(removed)) => MiscVersioning::Removed { deprecated, removed },
|
|
||||||
|
|
||||||
(None, Some(_)) => unreachable!("removed implies deprecated"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let entries = [unstable, r0, stable].into_iter().flatten().collect();
|
|
||||||
|
|
||||||
History { entries, misc }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum MiscVersioning {
|
|
||||||
None,
|
|
||||||
Deprecated(MatrixVersionLiteral),
|
|
||||||
Removed { deprecated: MatrixVersionLiteral, removed: MatrixVersionLiteral },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToTokens for History {
|
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
|
||||||
fn endpointpath_to_pathdata_ts(endpoint: &EndpointPath) -> String {
|
|
||||||
endpoint.value()
|
|
||||||
}
|
|
||||||
|
|
||||||
let unstable = self.entries.iter().filter_map(|e| match e {
|
|
||||||
HistoryEntry::Unstable { path } => Some(endpointpath_to_pathdata_ts(path)),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let versioned = self.entries.iter().filter_map(|e| match e {
|
|
||||||
HistoryEntry::Stable { path, version } => {
|
|
||||||
let path = endpointpath_to_pathdata_ts(path);
|
|
||||||
Some(quote! {( #version, #path )})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
let (deprecated, removed) = match &self.misc {
|
|
||||||
MiscVersioning::None => (None, None),
|
|
||||||
MiscVersioning::Deprecated(deprecated) => (Some(deprecated), None),
|
|
||||||
MiscVersioning::Removed { deprecated, removed } => (Some(deprecated), Some(removed)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let deprecated = util::map_option_literal(&deprecated);
|
|
||||||
let removed = util::map_option_literal(&removed);
|
|
||||||
|
|
||||||
tokens.extend(quote! {
|
|
||||||
::ruma_common::api::VersionHistory::new(
|
|
||||||
&[ #(#unstable),* ],
|
|
||||||
&[ #(#versioned),* ],
|
|
||||||
#deprecated,
|
|
||||||
#removed,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
// Unused variants will be constructed when the macro input is updated
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum HistoryEntry {
|
|
||||||
Unstable { path: EndpointPath },
|
|
||||||
Stable { version: MatrixVersionLiteral, path: EndpointPath },
|
|
||||||
Deprecated { version: MatrixVersionLiteral },
|
|
||||||
Removed { version: MatrixVersionLiteral },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct EndpointPath(LitStr);
|
|
||||||
|
|
||||||
impl EndpointPath {
|
|
||||||
pub fn value(&self) -> String {
|
|
||||||
self.0.value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for EndpointPath {
|
|
||||||
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
||||||
let path: LitStr = input.parse()?;
|
|
||||||
|
|
||||||
if util::is_valid_endpoint_path(&path.value()) {
|
|
||||||
Ok(Self(path))
|
|
||||||
} else {
|
|
||||||
Err(syn::Error::new_spanned(
|
|
||||||
&path,
|
|
||||||
"path may only contain printable ASCII characters with no spaces",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToTokens for EndpointPath {
|
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
|
||||||
self.0.to_tokens(tokens);
|
|
||||||
}
|
|
||||||
}
|
|
@ -53,9 +53,8 @@ impl Response {
|
|||||||
}
|
}
|
||||||
ResponseFieldKind::Header(header_name) => {
|
ResponseFieldKind::Header(header_name) => {
|
||||||
let optional_header = match &field.ty {
|
let optional_header = match &field.ty {
|
||||||
syn::Type::Path(syn::TypePath {
|
Type::Path(syn::TypePath {
|
||||||
path: syn::Path { segments, .. },
|
path: syn::Path { segments, .. }, ..
|
||||||
..
|
|
||||||
}) if segments.last().unwrap().ident == "Option" => {
|
}) if segments.last().unwrap().ident == "Option" => {
|
||||||
quote! {
|
quote! {
|
||||||
#( #cfg_attrs )*
|
#( #cfg_attrs )*
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
//! Functions to aid the `Api::to_tokens` method.
|
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::{quote, ToTokens};
|
|
||||||
|
|
||||||
pub fn map_option_literal<T: ToTokens>(ver: &Option<T>) -> TokenStream {
|
|
||||||
match ver {
|
|
||||||
Some(v) => quote! { ::std::option::Option::Some(#v) },
|
|
||||||
None => quote! { ::std::option::Option::None },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_valid_endpoint_path(string: &str) -> bool {
|
|
||||||
string.as_bytes().iter().all(|b| (0x21..=0x7E).contains(b))
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
use std::num::NonZeroU8;
|
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::{format_ident, quote, ToTokens};
|
|
||||||
use syn::{parse::Parse, Error, LitFloat};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct MatrixVersionLiteral {
|
|
||||||
pub(crate) major: NonZeroU8,
|
|
||||||
pub(crate) minor: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1) };
|
|
||||||
|
|
||||||
impl MatrixVersionLiteral {
|
|
||||||
pub const V1_0: Self = Self { major: ONE, minor: 0 };
|
|
||||||
pub const V1_1: Self = Self { major: ONE, minor: 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for MatrixVersionLiteral {
|
|
||||||
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
|
|
||||||
let fl: LitFloat = input.parse()?;
|
|
||||||
|
|
||||||
if !fl.suffix().is_empty() {
|
|
||||||
return Err(Error::new_spanned(
|
|
||||||
fl,
|
|
||||||
"matrix version has to be only two positive numbers separated by a `.`",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let ver_vec: Vec<String> = fl.to_string().split('.').map(&str::to_owned).collect();
|
|
||||||
|
|
||||||
let ver: [String; 2] = ver_vec.try_into().map_err(|_| {
|
|
||||||
Error::new_spanned(&fl, "did not contain only both an X and Y value like X.Y")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let major: NonZeroU8 = ver[0].parse().map_err(|e| {
|
|
||||||
Error::new_spanned(&fl, format!("major number failed to parse as >0 number: {e}"))
|
|
||||||
})?;
|
|
||||||
let minor: u8 = ver[1]
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| Error::new_spanned(&fl, format!("minor number failed to parse: {e}")))?;
|
|
||||||
|
|
||||||
Ok(Self { major, minor })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToTokens for MatrixVersionLiteral {
|
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
|
||||||
let variant = format_ident!("V{}_{}", u8::from(self.major), self.minor);
|
|
||||||
tokens.extend(quote! { ::ruma_common::api::MatrixVersion::#variant });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,9 @@
|
|||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
# 0.10.1
|
||||||
|
|
||||||
|
Upgrade `ruma-events` to 0.28.1.
|
||||||
|
|
||||||
# 0.10.0
|
# 0.10.0
|
||||||
|
|
||||||
- Bump MSRV to 1.75
|
- Bump MSRV to 1.75
|
||||||
|
@ -7,7 +7,7 @@ homepage = "https://ruma.dev/"
|
|||||||
repository = "https://github.com/ruma/ruma"
|
repository = "https://github.com/ruma/ruma"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
version = "0.10.0"
|
version = "0.10.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = { workspace = true }
|
rust-version = { workspace = true }
|
||||||
|
|
||||||
@ -30,22 +30,11 @@ client-hyper = ["client", "ruma-client?/hyper"]
|
|||||||
client-hyper-native-tls = ["client", "ruma-client?/hyper-native-tls"]
|
client-hyper-native-tls = ["client", "ruma-client?/hyper-native-tls"]
|
||||||
client-reqwest = ["client", "ruma-client?/reqwest"]
|
client-reqwest = ["client", "ruma-client?/reqwest"]
|
||||||
client-reqwest-native-tls = ["client", "ruma-client?/reqwest-native-tls"]
|
client-reqwest-native-tls = ["client", "ruma-client?/reqwest-native-tls"]
|
||||||
client-reqwest-native-tls-vendored = [
|
client-reqwest-native-tls-alpn = ["client", "ruma-client?/reqwest-native-tls-alpn"]
|
||||||
"client",
|
client-reqwest-native-tls-vendored = ["client", "ruma-client?/reqwest-native-tls-vendored"]
|
||||||
"ruma-client?/reqwest-native-tls-vendored",
|
client-reqwest-rustls-manual-roots = ["client", "ruma-client?/reqwest-rustls-manual-roots"]
|
||||||
]
|
client-reqwest-rustls-webpki-roots = ["client", "ruma-client?/reqwest-rustls-webpki-roots"]
|
||||||
client-reqwest-rustls-manual-roots = [
|
client-reqwest-rustls-native-roots = ["client", "ruma-client?/reqwest-rustls-native-roots"]
|
||||||
"client",
|
|
||||||
"ruma-client?/reqwest-rustls-manual-roots",
|
|
||||||
]
|
|
||||||
client-reqwest-rustls-webpki-roots = [
|
|
||||||
"client",
|
|
||||||
"ruma-client?/reqwest-rustls-webpki-roots",
|
|
||||||
]
|
|
||||||
client-reqwest-rustls-native-roots = [
|
|
||||||
"client",
|
|
||||||
"ruma-client?/reqwest-rustls-native-roots",
|
|
||||||
]
|
|
||||||
|
|
||||||
appservice-api-c = [
|
appservice-api-c = [
|
||||||
"api",
|
"api",
|
||||||
@ -214,6 +203,7 @@ unstable-exhaustive-types = [
|
|||||||
"ruma-federation-api?/unstable-exhaustive-types",
|
"ruma-federation-api?/unstable-exhaustive-types",
|
||||||
"ruma-identity-service-api?/unstable-exhaustive-types",
|
"ruma-identity-service-api?/unstable-exhaustive-types",
|
||||||
"ruma-push-gateway-api?/unstable-exhaustive-types",
|
"ruma-push-gateway-api?/unstable-exhaustive-types",
|
||||||
|
"ruma-signatures?/unstable-exhaustive-types",
|
||||||
"ruma-state-res?/unstable-exhaustive-types",
|
"ruma-state-res?/unstable-exhaustive-types",
|
||||||
"ruma-events?/unstable-exhaustive-types",
|
"ruma-events?/unstable-exhaustive-types",
|
||||||
]
|
]
|
||||||
@ -274,6 +264,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-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-pdu = ["ruma-events?/unstable-pdu"]
|
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
||||||
@ -327,6 +318,7 @@ __ci = [
|
|||||||
"unstable-msc3956",
|
"unstable-msc3956",
|
||||||
"unstable-msc3983",
|
"unstable-msc3983",
|
||||||
"unstable-msc4075",
|
"unstable-msc4075",
|
||||||
|
"unstable-msc4108",
|
||||||
"unstable-msc4121",
|
"unstable-msc4121",
|
||||||
"unstable-msc4125",
|
"unstable-msc4125",
|
||||||
]
|
]
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ruma = { version = "0.10.0", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] }
|
ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] }
|
||||||
|
|
||||||
anyhow = "1.0.37"
|
anyhow = "1.0.37"
|
||||||
tokio = { version = "1.0.1", features = ["macros", "rt"] }
|
tokio = { version = "1.0.1", features = ["macros", "rt"] }
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ruma = { version = "0.10.0", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] }
|
ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] }
|
||||||
# For building locally: use the git dependencies below.
|
# For building locally: use the git dependencies below.
|
||||||
# Browse the source at this revision here: https://github.com/ruma/ruma/tree/f161c8117c706fc52089999e1f406cf34276ec9d
|
# Browse the source at this revision here: https://github.com/ruma/ruma/tree/f161c8117c706fc52089999e1f406cf34276ec9d
|
||||||
# ruma = { git = "https://github.com/ruma/ruma", rev = "f161c8117c706fc52089999e1f406cf34276ec9d", features = ["client-api-c", "client", "client-hyper-native-tls", "events"] }
|
# ruma = { git = "https://github.com/ruma/ruma", rev = "f161c8117c706fc52089999e1f406cf34276ec9d", features = ["client-api-c", "client", "client-hyper-native-tls", "events"] }
|
||||||
|
@ -6,6 +6,6 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.37"
|
anyhow = "1.0.37"
|
||||||
ruma = { version = "0.10.0", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls"] }
|
ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls"] }
|
||||||
tokio = { version = "1.0.1", features = ["macros", "rt"] }
|
tokio = { version = "1.0.1", features = ["macros", "rt"] }
|
||||||
tokio-stream = { version = "0.1.1", default-features = false }
|
tokio-stream = { version = "0.1.1", default-features = false }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
# Keep in sync with version in `xtask/src/ci.rs` and `.github/workflows/ci.yml`
|
# Keep in sync with version in `xtask/src/ci.rs` and `.github/workflows/ci.yml`
|
||||||
channel = "nightly-2024-02-14"
|
channel = "nightly-2024-05-09"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
use std::path::PathBuf;
|
#![allow(clippy::disallowed_types)]
|
||||||
|
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{de::IgnoredAny, Deserialize};
|
use serde::{de::IgnoredAny, Deserialize};
|
||||||
|
#[cfg(feature = "default")]
|
||||||
use toml_edit::{value, Document};
|
use toml_edit::{value, Document};
|
||||||
|
#[cfg(feature = "default")]
|
||||||
use xshell::{cmd, pushd, read_file, write_file};
|
use xshell::{cmd, pushd, read_file, write_file};
|
||||||
|
|
||||||
use crate::{util::ask_yes_no, Metadata, Result};
|
use crate::{util::ask_yes_no, Metadata, Result};
|
||||||
@ -22,11 +27,51 @@ pub struct Package {
|
|||||||
/// The package's manifest path.
|
/// The package's manifest path.
|
||||||
pub manifest_path: PathBuf,
|
pub manifest_path: PathBuf,
|
||||||
|
|
||||||
/// A map of the package dependencies.
|
/// A list of the package dependencies.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dependencies: Vec<Dependency>,
|
pub dependencies: Vec<Dependency>,
|
||||||
|
|
||||||
|
/// A map of the package features.
|
||||||
|
#[serde(default)]
|
||||||
|
pub features: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Package {
|
||||||
|
/// Whether this package has a way to enable the given feature from the given package.
|
||||||
|
pub fn can_enable_feature(&self, package_name: &str, feature_name: &str) -> bool {
|
||||||
|
for activated_feature in self.features.values().flatten() {
|
||||||
|
// Remove optional `dep:` at the start.
|
||||||
|
let remaining = activated_feature.trim_start_matches("dep:");
|
||||||
|
|
||||||
|
// Check that we have the package name.
|
||||||
|
let Some(remaining) = remaining.strip_prefix(package_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if remaining.is_empty() {
|
||||||
|
// The feature only enables the dependency.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove optional `?`.
|
||||||
|
let remaining = remaining.trim_start_matches('?');
|
||||||
|
|
||||||
|
let Some(remaining) = remaining.strip_prefix('/') else {
|
||||||
|
// This is another package name starting with the same string.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finally, only the feature name is remaining.
|
||||||
|
if remaining == feature_name {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
impl Package {
|
impl Package {
|
||||||
/// Update the version of this crate.
|
/// Update the version of this crate.
|
||||||
pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
|
pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
|
||||||
@ -203,6 +248,7 @@ pub enum DependencyKind {
|
|||||||
Build,
|
Build,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
|
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CratesIoCrate {
|
struct CratesIoCrate {
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
|
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
|
||||||
#![allow(unused_qualifications)]
|
#![allow(unused_qualifications)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
|
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
use xshell::pushd;
|
use xshell::pushd;
|
||||||
|
|
||||||
use crate::{cmd, Metadata, Result, NIGHTLY};
|
use crate::{cmd, Metadata, Result, NIGHTLY};
|
||||||
|
|
||||||
|
mod reexport_features;
|
||||||
mod spec_links;
|
mod spec_links;
|
||||||
|
|
||||||
|
use reexport_features::check_reexport_features;
|
||||||
use spec_links::check_spec_links;
|
use spec_links::check_spec_links;
|
||||||
|
|
||||||
const MSRV: &str = "1.75";
|
const MSRV: &str = "1.75";
|
||||||
@ -66,6 +68,8 @@ pub enum CiCmd {
|
|||||||
Dependencies,
|
Dependencies,
|
||||||
/// Check spec links point to a recent version (lint)
|
/// Check spec links point to a recent version (lint)
|
||||||
SpecLinks,
|
SpecLinks,
|
||||||
|
/// Check all cargo features of sub-crates can be enabled from ruma (lint)
|
||||||
|
ReexportFeatures,
|
||||||
/// Check typos
|
/// Check typos
|
||||||
Typos,
|
Typos,
|
||||||
}
|
}
|
||||||
@ -75,18 +79,22 @@ pub struct CiTask {
|
|||||||
/// Which command to run.
|
/// Which command to run.
|
||||||
cmd: Option<CiCmd>,
|
cmd: Option<CiCmd>,
|
||||||
|
|
||||||
/// The root of the workspace.
|
/// The metadata of the workspace.
|
||||||
project_root: PathBuf,
|
project_metadata: Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CiTask {
|
impl CiTask {
|
||||||
pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
|
pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
|
||||||
let project_root = Metadata::load()?.workspace_root;
|
let project_metadata = Metadata::load()?;
|
||||||
Ok(Self { cmd, project_root })
|
Ok(Self { cmd, project_metadata })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_root(&self) -> &Path {
|
||||||
|
&self.project_metadata.workspace_root
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(self) -> Result<()> {
|
pub(crate) fn run(self) -> Result<()> {
|
||||||
let _p = pushd(&self.project_root)?;
|
let _p = pushd(self.project_root())?;
|
||||||
|
|
||||||
match self.cmd {
|
match self.cmd {
|
||||||
Some(CiCmd::Msrv) => self.msrv()?,
|
Some(CiCmd::Msrv) => self.msrv()?,
|
||||||
@ -110,7 +118,8 @@ impl CiTask {
|
|||||||
Some(CiCmd::ClippyAll) => self.clippy_all()?,
|
Some(CiCmd::ClippyAll) => self.clippy_all()?,
|
||||||
Some(CiCmd::Lint) => self.lint()?,
|
Some(CiCmd::Lint) => self.lint()?,
|
||||||
Some(CiCmd::Dependencies) => self.dependencies()?,
|
Some(CiCmd::Dependencies) => self.dependencies()?,
|
||||||
Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root.join("crates"))?,
|
Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root().join("crates"))?,
|
||||||
|
Some(CiCmd::ReexportFeatures) => check_reexport_features(&self.project_metadata)?,
|
||||||
Some(CiCmd::Typos) => self.typos()?,
|
Some(CiCmd::Typos) => self.typos()?,
|
||||||
None => {
|
None => {
|
||||||
self.msrv()
|
self.msrv()
|
||||||
@ -229,7 +238,7 @@ impl CiTask {
|
|||||||
cmd!(
|
cmd!(
|
||||||
"
|
"
|
||||||
rustup run {NIGHTLY} cargo check
|
rustup run {NIGHTLY} cargo check
|
||||||
--workspace --all-features -Z unstable-options -Z check-cfg
|
--workspace --all-features -Z unstable-options
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
.env(
|
.env(
|
||||||
@ -301,9 +310,11 @@ impl CiTask {
|
|||||||
// Check dependencies being sorted
|
// Check dependencies being sorted
|
||||||
let dependencies_res = self.dependencies();
|
let dependencies_res = self.dependencies();
|
||||||
// Check that all links point to the same version of the spec
|
// Check that all links point to the same version of the spec
|
||||||
let spec_links_res = check_spec_links(&self.project_root.join("crates"));
|
let spec_links_res = check_spec_links(&self.project_root().join("crates"));
|
||||||
|
// Check that all cargo features of sub-crates can be enabled from ruma.
|
||||||
|
let reexport_features_res = check_reexport_features(&self.project_metadata);
|
||||||
|
|
||||||
dependencies_res.and(spec_links_res)
|
dependencies_res.and(spec_links_res).and(reexport_features_res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check the sorting of dependencies with the nightly version.
|
/// Check the sorting of dependencies with the nightly version.
|
||||||
|
54
xtask/src/ci/reexport_features.rs
Normal file
54
xtask/src/ci/reexport_features.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use crate::{Metadata, Result};
|
||||||
|
|
||||||
|
/// Check that the ruma crate allows to enable all the features of the other ruma-* crates.
|
||||||
|
///
|
||||||
|
/// For simplicity, this function assumes that:
|
||||||
|
///
|
||||||
|
/// - Those dependencies are not renamed.
|
||||||
|
/// - ruma does not use `default-features = false` on those dependencies.
|
||||||
|
///
|
||||||
|
/// This does not check if all features are re-exported individually, as that is not always wanted.
|
||||||
|
pub(crate) fn check_reexport_features(metadata: &Metadata) -> Result<()> {
|
||||||
|
println!("Checking all features can be enabled from ruma…");
|
||||||
|
let mut n_errors = 0;
|
||||||
|
|
||||||
|
let Some(ruma) = metadata.find_package("ruma") else {
|
||||||
|
return Err("ruma package not found in workspace".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
for package in ruma.dependencies.iter().filter_map(|dep| metadata.find_package(&dep.name)) {
|
||||||
|
println!("Checking features of {}…", package.name);
|
||||||
|
|
||||||
|
// Exclude ruma and xtask.
|
||||||
|
if !package.name.starts_with("ruma-") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter features that are enabled by other features of the same package.
|
||||||
|
let features = package.features.keys().filter(|feature_name| {
|
||||||
|
!package.features.values().flatten().any(|activated_feature| {
|
||||||
|
activated_feature.trim_start_matches("dep:") == *feature_name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
for feature_name in features {
|
||||||
|
// Let's assume that ruma never has `default-features = false`.
|
||||||
|
if feature_name == "default" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ruma.can_enable_feature(&package.name, feature_name) {
|
||||||
|
println!(r#" Missing feature "{}/{feature_name}""#, package.name);
|
||||||
|
n_errors += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n_errors > 0 {
|
||||||
|
// Visual aid to separate the end error message.
|
||||||
|
println!();
|
||||||
|
return Err(format!("Found {n_errors} missing features").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -15,9 +15,8 @@ use serde::Deserialize;
|
|||||||
use serde_json::from_str as from_json_str;
|
use serde_json::from_str as from_json_str;
|
||||||
|
|
||||||
// Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
|
// Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
|
||||||
const NIGHTLY: &str = "nightly-2024-02-14";
|
const NIGHTLY: &str = "nightly-2024-05-09";
|
||||||
|
|
||||||
#[cfg(feature = "default")]
|
|
||||||
mod cargo;
|
mod cargo;
|
||||||
mod ci;
|
mod ci;
|
||||||
mod doc;
|
mod doc;
|
||||||
@ -26,6 +25,7 @@ mod release;
|
|||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use cargo::Package;
|
||||||
use ci::{CiArgs, CiTask};
|
use ci::{CiArgs, CiTask};
|
||||||
use doc::DocTask;
|
use doc::DocTask;
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
@ -70,8 +70,7 @@ fn main() -> Result<()> {
|
|||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
struct Metadata {
|
struct Metadata {
|
||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
#[cfg(feature = "default")]
|
pub packages: Vec<Package>,
|
||||||
pub packages: Vec<cargo::Package>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metadata {
|
impl Metadata {
|
||||||
@ -80,6 +79,11 @@ impl Metadata {
|
|||||||
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
|
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
|
||||||
Ok(from_json_str(&metadata_json)?)
|
Ok(from_json_str(&metadata_json)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the package with the given name.
|
||||||
|
pub fn find_package(&self, name: &str) -> Option<&Package> {
|
||||||
|
self.packages.iter().find(|p| p.name == name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user