Merge remote-tracking branch 'upstream/main' into conduwuit-changes
This commit is contained in:
commit
b6f82a72b6
@ -4,40 +4,5 @@ xtask = "run --package xtask --"
|
||||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
||||
|
||||
[target.'cfg(all())']
|
||||
rustflags = [
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wsemicolon_in_expressions_from_macros",
|
||||
"-Wunreachable_pub",
|
||||
"-Wunused_import_braces",
|
||||
"-Wunused_qualifications",
|
||||
"-Wclippy::branches_sharing_code",
|
||||
"-Wclippy::cloned_instead_of_copied",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::disallowed_types",
|
||||
"-Wclippy::empty_line_after_outer_attr",
|
||||
"-Wclippy::exhaustive_enums",
|
||||
"-Wclippy::exhaustive_structs",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::map_flatten",
|
||||
"-Wclippy::missing_enforced_import_renames",
|
||||
# Disabled because it triggers for tests/foo/mod.rs which can't be replaced
|
||||
# easily. Locally allowing it also doesn't seem to work.
|
||||
#"-Wclippy::mod_module_files",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Aclippy::new_without_default",
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::semicolon_if_nothing_returned",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
"-Wclippy::unreadable_literal",
|
||||
"-Wclippy::unseparated_literal_suffix",
|
||||
"-Wclippy::wildcard_imports",
|
||||
# Disabled temporarily because it triggers false positives for types with
|
||||
# generics.
|
||||
"-Aclippy::arc_with_non_send_sync",
|
||||
]
|
||||
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -50,10 +50,10 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Check spelling
|
||||
uses: crate-ci/typos@v1.20.3
|
||||
uses: crate-ci/typos@v1.24.5
|
||||
|
||||
- name: Install cargo-sort
|
||||
uses: taiki-e/cache-cargo-install-action@v1
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: cargo-sort
|
||||
|
||||
|
4
.github/workflows/deps.yml
vendored
4
.github/workflows/deps.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check bans licenses sources
|
||||
|
||||
@ -31,6 +31,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check advisories
|
||||
|
36
Cargo.toml
36
Cargo.toml
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/*", "examples/*", "xtask"]
|
||||
members = ["crates/*", "xtask"]
|
||||
# Only compile / check / document the public crates by default
|
||||
default-members = ["crates/*"]
|
||||
resolver = "2"
|
||||
@ -40,6 +40,40 @@ tracing = { version = "0.1.37", default-features = false, features = ["std"] }
|
||||
url = { version = "2.5.0" }
|
||||
web-time = "1.1.0"
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
branches_sharing_code = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
disallowed_types = "warn"
|
||||
empty_line_after_outer_attr = "warn"
|
||||
exhaustive_enums = "warn"
|
||||
exhaustive_structs = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
map_flatten = "warn"
|
||||
missing_enforced_import_renames = "warn"
|
||||
mod_module_files = "warn"
|
||||
mut_mut = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unreadable_literal = "warn"
|
||||
unseparated_literal_suffix = "warn"
|
||||
wildcard_imports = "warn"
|
||||
|
||||
# Not that good of a lint
|
||||
new_without_default = "allow"
|
||||
# Disabled temporarily because it triggers false positives for types with generics.
|
||||
arc_with_non_send_sync = "allow"
|
||||
|
||||
[profile.dev]
|
||||
# Speeds up test times by more than 10% in a simple test
|
||||
# Set to 1 or 2 if you want to use a debugger in this workspace
|
||||
|
@ -33,6 +33,9 @@ ruma = { git = "https://github.com/ruma/ruma", branch = "main", features = ["...
|
||||
them as a user. Check out the documentation [on docs.rs][docs] (or on
|
||||
[docs.ruma.dev][unstable-docs] if you use use the git dependency).
|
||||
|
||||
You can also find a small number of examples in our dedicated
|
||||
[examples repository](https://github.com/ruma/examples).
|
||||
|
||||
[matrix-rust-sdk]: https://github.com/matrix-org/matrix-rust-sdk#readme
|
||||
[feat]: https://github.com/ruma/ruma/blob/1166af5a354210dcbced1eaf4a11f795c381d2ec/ruma/Cargo.toml#L35
|
||||
|
||||
|
@ -32,3 +32,6 @@ serde_json = { workspace = true }
|
||||
[dev-dependencies]
|
||||
assert_matches2 = { workspace = true }
|
||||
serde_yaml = "0.9.14"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -10,6 +10,8 @@ Breaking changes:
|
||||
- The `content_disposition` fields of `media::get_content::v3::Response` and
|
||||
`media::get_content_as_filename::v3::Response` use now the strongly typed
|
||||
`ContentDisposition` instead of strings.
|
||||
- Replace `server_name` on `knock::knock_room::v3::Request` and
|
||||
`membership::join_room_by_id_or_alias::v3::Request` with `via` as per MSC4156.
|
||||
|
||||
Improvements:
|
||||
|
||||
|
@ -51,6 +51,7 @@ unstable-msc3983 = []
|
||||
unstable-msc4108 = []
|
||||
unstable-msc4121 = []
|
||||
unstable-msc4140 = []
|
||||
unstable-msc4186 = []
|
||||
|
||||
[dependencies]
|
||||
as_variant = { workspace = true }
|
||||
@ -72,3 +73,6 @@ web-time = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -176,7 +176,7 @@ pub enum ErrorKind {
|
||||
CannotOverwriteMedia,
|
||||
|
||||
/// M_UNKNOWN_POS for sliding sync
|
||||
#[cfg(feature = "unstable-msc3575")]
|
||||
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
|
||||
UnknownPos,
|
||||
|
||||
/// M_URL_NOT_SET
|
||||
@ -275,7 +275,7 @@ impl AsRef<str> for ErrorKind {
|
||||
Self::DuplicateAnnotation => "M_DUPLICATE_ANNOTATION",
|
||||
Self::NotYetUploaded => "M_NOT_YET_UPLOADED",
|
||||
Self::CannotOverwriteMedia => "M_CANNOT_OVERWRITE_MEDIA",
|
||||
#[cfg(feature = "unstable-msc3575")]
|
||||
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
|
||||
Self::UnknownPos => "M_UNKNOWN_POS",
|
||||
Self::UrlNotSet => "M_URL_NOT_SET",
|
||||
Self::BadStatus { .. } => "M_BAD_STATUS",
|
||||
|
@ -229,7 +229,7 @@ impl<'de> Visitor<'de> for ErrorKindVisitor {
|
||||
ErrCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation,
|
||||
ErrCode::NotYetUploaded => ErrorKind::NotYetUploaded,
|
||||
ErrCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia,
|
||||
#[cfg(feature = "unstable-msc3575")]
|
||||
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
|
||||
ErrCode::UnknownPos => ErrorKind::UnknownPos,
|
||||
ErrCode::UrlNotSet => ErrorKind::UrlNotSet,
|
||||
ErrCode::BadStatus => ErrorKind::BadStatus {
|
||||
@ -303,7 +303,7 @@ enum ErrCode {
|
||||
NotYetUploaded,
|
||||
#[ruma_enum(alias = "FI.MAU.MSC2246_CANNOT_OVERWRITE_MEDIA")]
|
||||
CannotOverwriteMedia,
|
||||
#[cfg(feature = "unstable-msc3575")]
|
||||
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
|
||||
UnknownPos,
|
||||
UrlNotSet,
|
||||
BadStatus,
|
||||
|
@ -8,7 +8,7 @@ pub mod v3 {
|
||||
//! [spec]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3knockroomidoralias
|
||||
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
api::{response, Metadata},
|
||||
metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
|
||||
};
|
||||
|
||||
@ -23,22 +23,137 @@ pub mod v3 {
|
||||
};
|
||||
|
||||
/// Request type for the `knock_room` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Request {
|
||||
/// The room the user should knock on.
|
||||
#[ruma_api(path)]
|
||||
pub room_id_or_alias: OwnedRoomOrAliasId,
|
||||
|
||||
/// The reason for joining a room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
|
||||
/// The servers to attempt to knock on the room through.
|
||||
///
|
||||
/// One of the servers must be participating in the room.
|
||||
#[ruma_api(query)]
|
||||
///
|
||||
/// When serializing, this field is mapped to both `server_name` and `via`
|
||||
/// with identical values.
|
||||
///
|
||||
/// When deserializing, the value is read from `via` if it's not missing or
|
||||
/// empty and `server_name` otherwise.
|
||||
pub via: Vec<OwnedServerName>,
|
||||
}
|
||||
|
||||
/// Data in the request's query string.
|
||||
#[cfg_attr(feature = "client", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "server", derive(serde::Deserialize))]
|
||||
struct RequestQuery {
|
||||
/// The servers to attempt to knock on the room through.
|
||||
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
|
||||
pub server_name: Vec<OwnedServerName>,
|
||||
via: Vec<OwnedServerName>,
|
||||
|
||||
/// The servers to attempt to knock on the room through.
|
||||
///
|
||||
/// Deprecated in Matrix >1.11 in favour of `via`.
|
||||
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
|
||||
server_name: Vec<OwnedServerName>,
|
||||
}
|
||||
|
||||
/// Data in the request's body.
|
||||
#[cfg_attr(feature = "client", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "server", derive(serde::Deserialize))]
|
||||
struct RequestBody {
|
||||
/// The reason for joining a room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
reason: Option<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,
|
||||
access_token: ruma_common::api::SendAccessToken<'_>,
|
||||
considering_versions: &'_ [ruma_common::api::MatrixVersion],
|
||||
) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
|
||||
use http::header::{self, HeaderValue};
|
||||
|
||||
let query_string = serde_html_form::to_string(RequestQuery {
|
||||
server_name: self.via.clone(),
|
||||
via: self.via,
|
||||
})?;
|
||||
|
||||
let http_request = http::Request::builder()
|
||||
.method(METADATA.method)
|
||||
.uri(METADATA.make_endpoint_url(
|
||||
considering_versions,
|
||||
base_url,
|
||||
&[&self.room_id_or_alias],
|
||||
&query_string,
|
||||
)?)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!(
|
||||
"Bearer {}",
|
||||
access_token
|
||||
.get_required_for_endpoint()
|
||||
.ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
|
||||
))?,
|
||||
)
|
||||
.body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?;
|
||||
|
||||
Ok(http_request)
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
{
|
||||
if request.method() != METADATA.method {
|
||||
return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
|
||||
expected: METADATA.method,
|
||||
received: request.method().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let (room_id_or_alias,) =
|
||||
serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
|
||||
_,
|
||||
serde::de::value::Error,
|
||||
>::new(
|
||||
path_args.iter().map(::std::convert::AsRef::as_ref),
|
||||
))?;
|
||||
|
||||
let request_query: RequestQuery =
|
||||
serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
|
||||
let via = if request_query.via.is_empty() {
|
||||
request_query.server_name
|
||||
} else {
|
||||
request_query.via
|
||||
};
|
||||
|
||||
let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
|
||||
|
||||
Ok(Self { room_id_or_alias, reason: body.reason, via })
|
||||
}
|
||||
}
|
||||
|
||||
/// Response type for the `knock_room` endpoint.
|
||||
@ -51,7 +166,7 @@ pub mod v3 {
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given room ID or alias.
|
||||
pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
|
||||
Self { room_id_or_alias, reason: None, server_name: vec![] }
|
||||
Self { room_id_or_alias, reason: None, via: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,4 +176,97 @@ pub mod v3 {
|
||||
Self { room_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, any(feature = "client", feature = "server")))]
|
||||
mod tests {
|
||||
use ruma_common::{
|
||||
api::{IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken},
|
||||
owned_room_id, owned_server_name,
|
||||
};
|
||||
|
||||
use super::Request;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
#[test]
|
||||
fn serialize_request() {
|
||||
let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
|
||||
req.via = vec![owned_server_name!("f.oo")];
|
||||
let req = req
|
||||
.try_into_http_request::<Vec<u8>>(
|
||||
"https://matrix.org",
|
||||
SendAccessToken::IfRequired("tok"),
|
||||
&[MatrixVersion::V1_1],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_wrong_method() {
|
||||
Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.expect_err("Should not deserialize request with illegal method");
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_only_via() {
|
||||
let req = Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(req.room_id_or_alias, "!foo:b.ar");
|
||||
assert_eq!(req.reason, Some("Let me in already!".to_owned()));
|
||||
assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_only_server_name() {
|
||||
let req = Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(req.room_id_or_alias, "!foo:b.ar");
|
||||
assert_eq!(req.reason, Some("Let me in already!".to_owned()));
|
||||
assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_via_and_server_name() {
|
||||
let req = Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(req.room_id_or_alias, "!foo:b.ar");
|
||||
assert_eq!(req.reason, Some("Let me in already!".to_owned()));
|
||||
assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ pub mod v3 {
|
||||
//! [spec]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3joinroomidoralias
|
||||
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
api::{response, Metadata},
|
||||
metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName,
|
||||
};
|
||||
|
||||
@ -25,27 +25,154 @@ pub mod v3 {
|
||||
};
|
||||
|
||||
/// Request type for the `join_room_by_id_or_alias` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Request {
|
||||
/// The room where the user should be invited.
|
||||
#[ruma_api(path)]
|
||||
pub room_id_or_alias: OwnedRoomOrAliasId,
|
||||
|
||||
/// The signature of a `m.third_party_invite` token to prove that this user owns a third
|
||||
/// party identity which has been invited to the room.
|
||||
pub third_party_signed: Option<ThirdPartySigned>,
|
||||
|
||||
/// Optional reason for joining the room.
|
||||
pub reason: Option<String>,
|
||||
|
||||
/// The servers to attempt to join the room through.
|
||||
///
|
||||
/// One of the servers must be participating in the room.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
|
||||
pub server_name: Vec<OwnedServerName>,
|
||||
///
|
||||
/// When serializing, this field is mapped to both `server_name` and `via`
|
||||
/// with identical values.
|
||||
///
|
||||
/// When deserializing, the value is read from `via` if it's not missing or
|
||||
/// empty and `server_name` otherwise.
|
||||
pub via: Vec<OwnedServerName>,
|
||||
}
|
||||
|
||||
/// Data in the request's query string.
|
||||
#[cfg_attr(feature = "client", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "server", derive(serde::Deserialize))]
|
||||
struct RequestQuery {
|
||||
/// The servers to attempt to join the room through.
|
||||
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
|
||||
via: Vec<OwnedServerName>,
|
||||
|
||||
/// The servers to attempt to join the room through.
|
||||
///
|
||||
/// Deprecated in Matrix >1.11 in favour of `via`.
|
||||
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
|
||||
server_name: Vec<OwnedServerName>,
|
||||
}
|
||||
|
||||
/// Data in the request's body.
|
||||
#[cfg_attr(feature = "client", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "server", derive(serde::Deserialize))]
|
||||
struct RequestBody {
|
||||
/// The signature of a `m.third_party_invite` token to prove that this user owns a third
|
||||
/// party identity which has been invited to the room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub third_party_signed: Option<ThirdPartySigned>,
|
||||
third_party_signed: Option<ThirdPartySigned>,
|
||||
|
||||
/// Optional reason for joining the room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
reason: Option<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,
|
||||
access_token: ruma_common::api::SendAccessToken<'_>,
|
||||
considering_versions: &'_ [ruma_common::api::MatrixVersion],
|
||||
) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
|
||||
use http::header::{self, HeaderValue};
|
||||
|
||||
let query_string = serde_html_form::to_string(RequestQuery {
|
||||
server_name: self.via.clone(),
|
||||
via: self.via,
|
||||
})?;
|
||||
|
||||
let http_request = http::Request::builder()
|
||||
.method(METADATA.method)
|
||||
.uri(METADATA.make_endpoint_url(
|
||||
considering_versions,
|
||||
base_url,
|
||||
&[&self.room_id_or_alias],
|
||||
&query_string,
|
||||
)?)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!(
|
||||
"Bearer {}",
|
||||
access_token
|
||||
.get_required_for_endpoint()
|
||||
.ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
|
||||
))?,
|
||||
)
|
||||
.body(ruma_common::serde::json_to_buf(&RequestBody {
|
||||
third_party_signed: self.third_party_signed,
|
||||
reason: self.reason,
|
||||
})?)?;
|
||||
|
||||
Ok(http_request)
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
{
|
||||
if request.method() != METADATA.method {
|
||||
return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch {
|
||||
expected: METADATA.method,
|
||||
received: request.method().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let (room_id_or_alias,) =
|
||||
serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
|
||||
_,
|
||||
serde::de::value::Error,
|
||||
>::new(
|
||||
path_args.iter().map(::std::convert::AsRef::as_ref),
|
||||
))?;
|
||||
|
||||
let request_query: RequestQuery =
|
||||
serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
|
||||
let via = if request_query.via.is_empty() {
|
||||
request_query.server_name
|
||||
} else {
|
||||
request_query.via
|
||||
};
|
||||
|
||||
let body: RequestBody = serde_json::from_slice(request.body().as_ref())?;
|
||||
|
||||
Ok(Self {
|
||||
room_id_or_alias,
|
||||
reason: body.reason,
|
||||
third_party_signed: body.third_party_signed,
|
||||
via,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Response type for the `join_room_by_id_or_alias` endpoint.
|
||||
@ -58,7 +185,7 @@ pub mod v3 {
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given room ID or alias ID.
|
||||
pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self {
|
||||
Self { room_id_or_alias, server_name: vec![], third_party_signed: None, reason: None }
|
||||
Self { room_id_or_alias, via: vec![], third_party_signed: None, reason: None }
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,4 +195,97 @@ pub mod v3 {
|
||||
Self { room_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, any(feature = "client", feature = "server")))]
|
||||
mod tests {
|
||||
use ruma_common::{
|
||||
api::{IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken},
|
||||
owned_room_id, owned_server_name,
|
||||
};
|
||||
|
||||
use super::Request;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
#[test]
|
||||
fn serialize_request() {
|
||||
let mut req = Request::new(owned_room_id!("!foo:b.ar").into());
|
||||
req.via = vec![owned_server_name!("f.oo")];
|
||||
let req = req
|
||||
.try_into_http_request::<Vec<u8>>(
|
||||
"https://matrix.org",
|
||||
SendAccessToken::IfRequired("tok"),
|
||||
&[MatrixVersion::V1_1],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_wrong_method() {
|
||||
Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.expect_err("Should not deserialize request with illegal method");
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_only_via() {
|
||||
let req = Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(req.room_id_or_alias, "!foo:b.ar");
|
||||
assert_eq!(req.reason, Some("Let me in already!".to_owned()));
|
||||
assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_only_server_name() {
|
||||
let req = Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?server_name=f.oo")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(req.room_id_or_alias, "!foo:b.ar");
|
||||
assert_eq!(req.reason, Some("Let me in already!".to_owned()));
|
||||
assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[test]
|
||||
fn deserialize_request_via_and_server_name() {
|
||||
let req = Request::try_from_http_request(
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo&server_name=b.ar")
|
||||
.body(b"{ \"reason\": \"Let me in already!\" }" as &[u8])
|
||||
.unwrap(),
|
||||
&["!foo:b.ar"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(req.room_id_or_alias, "!foo:b.ar");
|
||||
assert_eq!(req.reason, Some("Let me in already!".to_owned()));
|
||||
assert_eq!(req.via, vec![owned_server_name!("f.oo")]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ pub mod v3;
|
||||
#[cfg(feature = "unstable-msc3575")]
|
||||
pub mod v4;
|
||||
|
||||
#[cfg(feature = "unstable-msc4186")]
|
||||
pub mod v5;
|
||||
|
||||
/// Unread notifications count.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
|
@ -23,7 +23,7 @@ use ruma_events::{
|
||||
};
|
||||
use serde::{de::Error as _, Deserialize, Serialize};
|
||||
|
||||
use super::{DeviceLists, UnreadNotificationsCount};
|
||||
use super::{v5, DeviceLists, UnreadNotificationsCount};
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: POST,
|
||||
@ -394,7 +394,7 @@ pub enum SlidingOp {
|
||||
}
|
||||
|
||||
/// Updates to joined rooms.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct SyncList {
|
||||
/// The sync operation to apply, if any.
|
||||
@ -928,6 +928,141 @@ impl Typing {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::Request> for Request {
|
||||
fn from(value: v5::Request) -> Self {
|
||||
Self {
|
||||
pos: value.pos,
|
||||
conn_id: value.conn_id,
|
||||
txn_id: value.txn_id,
|
||||
timeout: value.timeout,
|
||||
lists: value
|
||||
.lists
|
||||
.into_iter()
|
||||
.map(|(list_name, list)| (list_name, list.into()))
|
||||
.collect(),
|
||||
room_subscriptions: value
|
||||
.room_subscriptions
|
||||
.into_iter()
|
||||
.map(|(room_id, room_subscription)| (room_id, room_subscription.into()))
|
||||
.collect(),
|
||||
extensions: value.extensions.into(),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::List> for SyncRequestList {
|
||||
fn from(value: v5::request::List) -> Self {
|
||||
Self {
|
||||
ranges: value.ranges,
|
||||
room_details: value.room_details.into(),
|
||||
include_heroes: value.include_heroes,
|
||||
filters: value.filters.map(Into::into),
|
||||
|
||||
// Defaults from MSC4186.
|
||||
sort: vec!["by_recency".to_owned(), "by_name".to_owned()],
|
||||
bump_event_types: vec![
|
||||
TimelineEventType::RoomMessage,
|
||||
TimelineEventType::RoomEncrypted,
|
||||
TimelineEventType::RoomCreate,
|
||||
TimelineEventType::Sticker,
|
||||
],
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::RoomDetails> for RoomDetailsConfig {
|
||||
fn from(value: v5::request::RoomDetails) -> Self {
|
||||
Self { required_state: value.required_state, timeline_limit: value.timeline_limit }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::ListFilters> for SyncRequestListFilters {
|
||||
fn from(value: v5::request::ListFilters) -> Self {
|
||||
Self {
|
||||
is_invite: value.is_invite,
|
||||
not_room_types: value.not_room_types,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::RoomSubscription> for RoomSubscription {
|
||||
fn from(value: v5::request::RoomSubscription) -> Self {
|
||||
Self {
|
||||
required_state: value.required_state,
|
||||
timeline_limit: value.timeline_limit,
|
||||
include_heroes: value.include_heroes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::Extensions> for ExtensionsConfig {
|
||||
fn from(value: v5::request::Extensions) -> Self {
|
||||
Self {
|
||||
to_device: value.to_device.into(),
|
||||
e2ee: value.e2ee.into(),
|
||||
account_data: value.account_data.into(),
|
||||
receipts: value.receipts.into(),
|
||||
typing: value.typing.into(),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::ToDevice> for ToDeviceConfig {
|
||||
fn from(value: v5::request::ToDevice) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
limit: value.limit,
|
||||
since: value.since,
|
||||
lists: value.lists,
|
||||
rooms: value.rooms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::E2EE> for E2EEConfig {
|
||||
fn from(value: v5::request::E2EE) -> Self {
|
||||
Self { enabled: value.enabled }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::AccountData> for AccountDataConfig {
|
||||
fn from(value: v5::request::AccountData) -> Self {
|
||||
Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::Receipts> for ReceiptsConfig {
|
||||
fn from(value: v5::request::Receipts) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
lists: value.lists,
|
||||
rooms: value.rooms.map(|rooms| rooms.into_iter().map(Into::into).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::ReceiptsRoom> for RoomReceiptConfig {
|
||||
fn from(value: v5::request::ReceiptsRoom) -> Self {
|
||||
match value {
|
||||
v5::request::ReceiptsRoom::Room(room_id) => Self::Room(room_id),
|
||||
_ => Self::AllSubscribed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v5::request::Typing> for TypingConfig {
|
||||
fn from(value: v5::request::Typing) -> Self {
|
||||
Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma_common::owned_room_id;
|
||||
|
856
crates/ruma-client-api/src/sync/sync_events/v5.rs
Normal file
856
crates/ruma-client-api/src/sync/sync_events/v5.rs
Normal file
@ -0,0 +1,856 @@
|
||||
//! `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync` ([MSC4186])
|
||||
//!
|
||||
//! A simplified version of sliding sync ([MSC3575]).
|
||||
//!
|
||||
//! Get all new events in a sliding window of rooms since the last sync or a given point in time.
|
||||
//!
|
||||
//! [MSC3575]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575
|
||||
//! [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
|
||||
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use js_int::UInt;
|
||||
use js_option::JsOption;
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
serde::{duration::opt_ms, Raw},
|
||||
OwnedMxcUri, OwnedRoomId, OwnedUserId,
|
||||
};
|
||||
use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{v4, UnreadNotificationsCount};
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: POST,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
unstable => "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync",
|
||||
// 1.4 => "/_matrix/client/v5/sync",
|
||||
}
|
||||
};
|
||||
|
||||
/// Request type for the `/sync` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[derive(Default)]
|
||||
pub struct Request {
|
||||
/// A point in time to continue a sync from.
|
||||
///
|
||||
/// This is an opaque value taken from the `pos` field of a previous `/sync`
|
||||
/// response. A `None` value asks the server to start a new _session_ (mind
|
||||
/// it can be costly)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ruma_api(query)]
|
||||
pub pos: Option<String>,
|
||||
|
||||
/// A unique string identifier for this connection to the server.
|
||||
///
|
||||
/// If this is missing, only one sliding sync connection can be made to
|
||||
/// the server at any one time. Clients need to set this to allow more
|
||||
/// than one connection concurrently, so the server can distinguish between
|
||||
/// connections. This must be provided with every request, if your client
|
||||
/// needs more than one concurrent connection.
|
||||
///
|
||||
/// Limitation: it must not contain more than 16 chars, due to it being
|
||||
/// required with every request.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conn_id: Option<String>,
|
||||
|
||||
/// Allows clients to know what request params reached the server,
|
||||
/// functionally similar to txn IDs on `/send` for events.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub txn_id: Option<String>,
|
||||
|
||||
/// The maximum time to poll before responding to this request.
|
||||
///
|
||||
/// `None` means no timeout, so virtually an infinite wait from the server.
|
||||
#[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")]
|
||||
#[ruma_api(query)]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// Lists of rooms we are interested by, represented by ranges.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub lists: BTreeMap<String, request::List>,
|
||||
|
||||
/// Specific rooms we are interested by.
|
||||
///
|
||||
/// It is useful to receive updates from rooms that are possibly
|
||||
/// out-of-range of all the lists (see [`Self::lists`]).
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub room_subscriptions: BTreeMap<OwnedRoomId, request::RoomSubscription>,
|
||||
|
||||
/// Extensions.
|
||||
#[serde(default, skip_serializing_if = "request::Extensions::is_empty")]
|
||||
pub extensions: request::Extensions,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Creates an empty `Request`.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP types related to a [`Request`].
|
||||
pub mod request {
|
||||
use ruma_common::{directory::RoomTypeFilter, serde::deserialize_cow_str, RoomId};
|
||||
use serde::de::Error as _;
|
||||
|
||||
use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt};
|
||||
|
||||
/// A sliding sync list request (see [`super::Request::lists`]).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct List {
|
||||
/// The ranges of rooms we're interested in.
|
||||
pub ranges: Vec<(UInt, UInt)>,
|
||||
|
||||
/// The details to be included per room.
|
||||
#[serde(flatten)]
|
||||
pub room_details: RoomDetails,
|
||||
|
||||
/// Request a stripped variant of membership events for the users used
|
||||
/// to calculate the room name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_heroes: Option<bool>,
|
||||
|
||||
/// Filters to apply to the list before sorting.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filters: Option<ListFilters>,
|
||||
}
|
||||
|
||||
/// A sliding sync list request filters (see [`List::filters`]).
|
||||
///
|
||||
/// All fields are applied with _AND_ operators. The absence of fields
|
||||
/// implies no filter on that criteria: it does NOT imply `false`.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct ListFilters {
|
||||
/// Whether to return invited rooms, only joined rooms or both.
|
||||
///
|
||||
/// Flag which only returns rooms the user is currently invited to.
|
||||
/// If unset, both invited and joined rooms are returned. If false,
|
||||
/// no invited rooms are returned. If true, only invited rooms are
|
||||
/// returned.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_invite: Option<bool>,
|
||||
|
||||
/// Only list rooms that are not of these create-types, or all.
|
||||
///
|
||||
/// This can be used to filter out spaces from the room list.
|
||||
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
|
||||
pub not_room_types: Vec<RoomTypeFilter>,
|
||||
}
|
||||
|
||||
/// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct RoomSubscription {
|
||||
/// Required state for each returned room. An array of event type and
|
||||
/// state key tuples.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub required_state: Vec<(StateEventType, String)>,
|
||||
|
||||
/// The maximum number of timeline events to return per room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeline_limit: Option<UInt>,
|
||||
|
||||
/// Include the room heroes.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_heroes: Option<bool>,
|
||||
}
|
||||
|
||||
/// Sliding sync request room details (see [`List::room_details`]).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct RoomDetails {
|
||||
/// Required state for each returned room. An array of event type and state key tuples.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub required_state: Vec<(StateEventType, String)>,
|
||||
|
||||
/// The maximum number of timeline events to return per room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeline_limit: Option<UInt>,
|
||||
}
|
||||
|
||||
/// Sliding sync request extensions (see [`super::Request::extensions`]).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Extensions {
|
||||
/// Configure the to-device extension.
|
||||
#[serde(default, skip_serializing_if = "ToDevice::is_empty")]
|
||||
pub to_device: ToDevice,
|
||||
|
||||
/// Configure the E2EE extension.
|
||||
#[serde(default, skip_serializing_if = "E2EE::is_empty")]
|
||||
pub e2ee: E2EE,
|
||||
|
||||
/// Configure the account data extension.
|
||||
#[serde(default, skip_serializing_if = "AccountData::is_empty")]
|
||||
pub account_data: AccountData,
|
||||
|
||||
/// Configure the receipts extension.
|
||||
#[serde(default, skip_serializing_if = "Receipts::is_empty")]
|
||||
pub receipts: Receipts,
|
||||
|
||||
/// Configure the typing extension.
|
||||
#[serde(default, skip_serializing_if = "Typing::is_empty")]
|
||||
pub typing: Typing,
|
||||
|
||||
/// Extensions may add further fields to the list.
|
||||
#[serde(flatten)]
|
||||
other: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Extensions {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.to_device.is_empty()
|
||||
&& self.e2ee.is_empty()
|
||||
&& self.account_data.is_empty()
|
||||
&& self.receipts.is_empty()
|
||||
&& self.typing.is_empty()
|
||||
&& self.other.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// To-device messages extension.
|
||||
///
|
||||
/// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct ToDevice {
|
||||
/// Activate or deactivate this extension.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// Maximum number of to-device messages per response.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<UInt>,
|
||||
|
||||
/// Give messages since this token only.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub since: Option<String>,
|
||||
|
||||
/// List of list names for which to-device events should be enabled.
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the lists appearing in the
|
||||
/// request. If defined and empty, will be disabled for all the lists.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lists: Option<Vec<String>>,
|
||||
|
||||
/// List of room names for which to-device events should be enabled.
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the rooms appearing in the
|
||||
/// room subscriptions. If defined and empty, will be disabled for all
|
||||
/// the rooms.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rooms: Option<Vec<OwnedRoomId>>,
|
||||
}
|
||||
|
||||
impl ToDevice {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// E2EE extension configuration.
|
||||
///
|
||||
/// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct E2EE {
|
||||
/// Activate or deactivate this extension.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl E2EE {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.enabled.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Account-data extension .
|
||||
///
|
||||
/// Not yet part of the spec proposal. Taken from the reference implementation
|
||||
/// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct AccountData {
|
||||
/// Activate or deactivate this extension.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// List of list names for which account data should be enabled.
|
||||
///
|
||||
/// This is specific to room account data (e.g. user-defined room tags).
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the lists appearing in the
|
||||
/// request. If defined and empty, will be disabled for all the lists.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lists: Option<Vec<String>>,
|
||||
|
||||
/// List of room names for which account data should be enabled.
|
||||
///
|
||||
/// This is specific to room account data (e.g. user-defined room tags).
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the rooms appearing in the
|
||||
/// room subscriptions. If defined and empty, will be disabled for all
|
||||
/// the rooms.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rooms: Option<Vec<OwnedRoomId>>,
|
||||
}
|
||||
|
||||
impl AccountData {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.enabled.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Receipt extension.
|
||||
///
|
||||
/// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Receipts {
|
||||
/// Activate or deactivate this extension.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// List of list names for which receipts should be enabled.
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the lists appearing in the
|
||||
/// request. If defined and empty, will be disabled for all the lists.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lists: Option<Vec<String>>,
|
||||
|
||||
/// List of room names for which receipts should be enabled.
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the rooms appearing in the
|
||||
/// room subscriptions. If defined and empty, will be disabled for all
|
||||
/// the rooms.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rooms: Option<Vec<ReceiptsRoom>>,
|
||||
}
|
||||
|
||||
impl Receipts {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.enabled.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Single entry for a room-related read receipt configuration in
|
||||
/// [`Receipts`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum ReceiptsRoom {
|
||||
/// Get read receipts for all the subscribed rooms.
|
||||
AllSubscribed,
|
||||
|
||||
/// Get read receipts for this particular room.
|
||||
Room(OwnedRoomId),
|
||||
}
|
||||
|
||||
impl Serialize for ReceiptsRoom {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::AllSubscribed => serializer.serialize_str("*"),
|
||||
Self::Room(r) => r.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReceiptsRoom {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
match deserialize_cow_str(deserializer)?.as_ref() {
|
||||
"*" => Ok(Self::AllSubscribed),
|
||||
other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Typing extension configuration.
|
||||
///
|
||||
/// Not yet part of the spec proposal. Taken from the reference implementation
|
||||
/// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Typing {
|
||||
/// Activate or deactivate this extension.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// List of list names for which typing notifications should be enabled.
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the lists appearing in the
|
||||
/// request. If defined and empty, will be disabled for all the lists.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lists: Option<Vec<String>>,
|
||||
|
||||
/// List of room names for which typing notifications should be enabled.
|
||||
///
|
||||
/// If not defined, will be enabled for *all* the rooms appearing in the
|
||||
/// room subscriptions. If defined and empty, will be disabled for all
|
||||
/// the rooms.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rooms: Option<Vec<OwnedRoomId>>,
|
||||
}
|
||||
|
||||
impl Typing {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.enabled.is_none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response type for the `/sync` endpoint.
|
||||
#[response(error = crate::Error)]
|
||||
pub struct Response {
|
||||
/// Whether this response describes an initial sync.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub initial: bool,
|
||||
|
||||
/// Matches the `txn_id` sent by the request (see [`Request::txn_id`]).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub txn_id: Option<String>,
|
||||
|
||||
/// The token to supply in the `pos` parameter of the next `/sync` request
|
||||
/// (see [`Request::pos`]).
|
||||
pub pos: String,
|
||||
|
||||
/// Resulting details of the lists.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub lists: BTreeMap<String, response::List>,
|
||||
|
||||
/// The updated rooms.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub rooms: BTreeMap<OwnedRoomId, response::Room>,
|
||||
|
||||
/// Extensions.
|
||||
#[serde(default, skip_serializing_if = "response::Extensions::is_empty")]
|
||||
pub extensions: response::Extensions,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response` with the given `pos`.
|
||||
pub fn new(pos: String) -> Self {
|
||||
Self {
|
||||
initial: Default::default(),
|
||||
txn_id: None,
|
||||
pos,
|
||||
lists: Default::default(),
|
||||
rooms: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP types related to a [`Response`].
|
||||
pub mod response {
|
||||
use ruma_common::DeviceKeyAlgorithm;
|
||||
use ruma_events::{
|
||||
receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent,
|
||||
AnyRoomAccountDataEvent, AnyToDeviceEvent,
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::DeviceLists, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
BTreeMap, Deserialize, JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize,
|
||||
UInt, UnreadNotificationsCount,
|
||||
};
|
||||
|
||||
/// A sliding sync response updates to joiend rooms (see
|
||||
/// [`super::Response::lists`]).
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct List {
|
||||
/// The total number of rooms found for this list.
|
||||
pub count: UInt,
|
||||
}
|
||||
|
||||
/// A slising sync response updated room (see [`super::Response::rooms`]).
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Room {
|
||||
/// The name as calculated by the server.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The avatar.
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
|
||||
pub avatar: JsOption<OwnedMxcUri>,
|
||||
|
||||
/// Whether it is an initial response.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial: Option<bool>,
|
||||
|
||||
/// Whether it is a direct room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_dm: Option<bool>,
|
||||
|
||||
/// If this is `Some(_)`, this is a not-yet-accepted invite containing
|
||||
/// the given stripped state events.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invite_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
|
||||
|
||||
/// Number of unread notifications.
|
||||
#[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
|
||||
pub unread_notifications: UnreadNotificationsCount,
|
||||
|
||||
/// Message-like events and live state events.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub timeline: Vec<Raw<AnySyncTimelineEvent>>,
|
||||
|
||||
/// State events as configured by the request.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub required_state: Vec<Raw<AnySyncStateEvent>>,
|
||||
|
||||
/// The `prev_batch` allowing you to paginate through the messages
|
||||
/// before the given ones.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prev_batch: Option<String>,
|
||||
|
||||
/// True if the number of events returned was limited by the limit on
|
||||
/// the filter.
|
||||
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
||||
pub limited: bool,
|
||||
|
||||
/// The number of users with membership of `join`, including the
|
||||
/// client’s own user ID.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub joined_count: Option<UInt>,
|
||||
|
||||
/// The number of users with membership of `invite`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invited_count: Option<UInt>,
|
||||
|
||||
/// The number of timeline events which have just occurred and are not
|
||||
/// historical.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_live: Option<UInt>,
|
||||
|
||||
/// The bump stamp of the room.
|
||||
///
|
||||
/// It can be interpreted as a “recency stamp” or “streaming order
|
||||
/// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB`
|
||||
/// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC`
|
||||
/// receives an update, its `bump_stamp` will be 3.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bump_stamp: Option<UInt>,
|
||||
|
||||
/// Heroes of the room, if requested.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub heroes: Option<Vec<Hero>>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// Creates an empty `Room`.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A sliding sync response room hero (see [`Room::heroes`]).
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Hero {
|
||||
/// The user ID.
|
||||
pub user_id: OwnedUserId,
|
||||
|
||||
/// The name.
|
||||
#[serde(rename = "displayname", skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The avatar.
|
||||
#[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<OwnedMxcUri>,
|
||||
}
|
||||
|
||||
impl Hero {
|
||||
/// Creates a new `Hero` with the given user ID.
|
||||
pub fn new(user_id: OwnedUserId) -> Self {
|
||||
Self { user_id, name: None, avatar: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions responses.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Extensions {
|
||||
/// To-device extension response.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub to_device: Option<ToDevice>,
|
||||
|
||||
/// E2EE extension response.
|
||||
#[serde(default, skip_serializing_if = "E2EE::is_empty")]
|
||||
pub e2ee: E2EE,
|
||||
|
||||
/// Account data extension response.
|
||||
#[serde(default, skip_serializing_if = "AccountData::is_empty")]
|
||||
pub account_data: AccountData,
|
||||
|
||||
/// Receipts extension response.
|
||||
#[serde(default, skip_serializing_if = "Receipts::is_empty")]
|
||||
pub receipts: Receipts,
|
||||
|
||||
/// Typing extension response.
|
||||
#[serde(default, skip_serializing_if = "Typing::is_empty")]
|
||||
pub typing: Typing,
|
||||
}
|
||||
|
||||
impl Extensions {
|
||||
/// Whether the extension data is empty.
|
||||
///
|
||||
/// True if neither to-device, e2ee nor account data are to be found.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.to_device.is_none()
|
||||
&& self.e2ee.is_empty()
|
||||
&& self.account_data.is_empty()
|
||||
&& self.receipts.is_empty()
|
||||
&& self.typing.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// To-device extension response.
|
||||
///
|
||||
/// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct ToDevice {
|
||||
/// Fetch the next batch from this entry.
|
||||
pub next_batch: String,
|
||||
|
||||
/// The to-device events.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
}
|
||||
|
||||
/// E2EE extension response.
|
||||
///
|
||||
/// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct E2EE {
|
||||
/// Information on E2EE device updates.
|
||||
#[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
|
||||
pub device_lists: DeviceLists,
|
||||
|
||||
/// For each key algorithm, the number of unclaimed one-time keys
|
||||
/// currently held on the server for a device.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, UInt>,
|
||||
|
||||
/// For each key algorithm, the number of unclaimed one-time keys
|
||||
/// currently held on the server for a device.
|
||||
///
|
||||
/// The presence of this field indicates that the server supports
|
||||
/// fallback keys.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_unused_fallback_key_types: Option<Vec<DeviceKeyAlgorithm>>,
|
||||
}
|
||||
|
||||
impl E2EE {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.device_lists.is_empty()
|
||||
&& self.device_one_time_keys_count.is_empty()
|
||||
&& self.device_unused_fallback_key_types.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Account-data extension response .
|
||||
///
|
||||
/// Not yet part of the spec proposal. Taken from the reference implementation
|
||||
/// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct AccountData {
|
||||
/// The global private data created by this user.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub global: Vec<Raw<AnyGlobalAccountDataEvent>>,
|
||||
|
||||
/// The private data that this user has attached to each room.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub rooms: BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
|
||||
}
|
||||
|
||||
impl AccountData {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.global.is_empty() && self.rooms.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Receipt extension response.
|
||||
///
|
||||
/// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Receipts {
|
||||
/// The ephemeral receipt room event for each room.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub rooms: BTreeMap<OwnedRoomId, Raw<SyncReceiptEvent>>,
|
||||
}
|
||||
|
||||
impl Receipts {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rooms.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Typing extension response.
|
||||
///
|
||||
/// Not yet part of the spec proposal. Taken from the reference implementation
|
||||
/// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct Typing {
|
||||
/// The ephemeral typing event for each room.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub rooms: BTreeMap<OwnedRoomId, Raw<SyncTypingEvent>>,
|
||||
}
|
||||
|
||||
impl Typing {
|
||||
/// Whether all fields are empty or `None`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rooms.is_empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::Response> for Response {
|
||||
fn from(value: v4::Response) -> Self {
|
||||
Self {
|
||||
pos: value.pos,
|
||||
initial: value.initial,
|
||||
txn_id: value.txn_id,
|
||||
lists: value.lists.into_iter().map(|(room_id, list)| (room_id, list.into())).collect(),
|
||||
rooms: value.rooms.into_iter().map(|(room_id, room)| (room_id, room.into())).collect(),
|
||||
extensions: value.extensions.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::SyncList> for response::List {
|
||||
fn from(value: v4::SyncList) -> Self {
|
||||
Self { count: value.count }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::SlidingSyncRoom> for response::Room {
|
||||
fn from(value: v4::SlidingSyncRoom) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
avatar: value.avatar,
|
||||
initial: value.initial,
|
||||
is_dm: value.is_dm,
|
||||
invite_state: value.invite_state,
|
||||
unread_notifications: value.unread_notifications,
|
||||
timeline: value.timeline,
|
||||
required_state: value.required_state,
|
||||
prev_batch: value.prev_batch,
|
||||
limited: value.limited,
|
||||
joined_count: value.joined_count,
|
||||
invited_count: value.invited_count,
|
||||
num_live: value.num_live,
|
||||
bump_stamp: value.timestamp.map(|t| t.0),
|
||||
heroes: value.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::SlidingSyncRoomHero> for response::Hero {
|
||||
fn from(value: v4::SlidingSyncRoomHero) -> Self {
|
||||
Self { user_id: value.user_id, name: value.name, avatar: value.avatar }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::Extensions> for response::Extensions {
|
||||
fn from(value: v4::Extensions) -> Self {
|
||||
Self {
|
||||
to_device: value.to_device.map(Into::into),
|
||||
e2ee: value.e2ee.into(),
|
||||
account_data: value.account_data.into(),
|
||||
receipts: value.receipts.into(),
|
||||
typing: value.typing.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::ToDevice> for response::ToDevice {
|
||||
fn from(value: v4::ToDevice) -> Self {
|
||||
Self { next_batch: value.next_batch, events: value.events }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::E2EE> for response::E2EE {
|
||||
fn from(value: v4::E2EE) -> Self {
|
||||
Self {
|
||||
device_lists: value.device_lists,
|
||||
device_one_time_keys_count: value.device_one_time_keys_count,
|
||||
device_unused_fallback_key_types: value.device_unused_fallback_key_types,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::AccountData> for response::AccountData {
|
||||
fn from(value: v4::AccountData) -> Self {
|
||||
Self { global: value.global, rooms: value.rooms }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::Receipts> for response::Receipts {
|
||||
fn from(value: v4::Receipts) -> Self {
|
||||
Self { rooms: value.rooms }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v4::Typing> for response::Typing {
|
||||
fn from(value: v4::Typing) -> Self {
|
||||
Self { rooms: value.rooms }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma_common::owned_room_id;
|
||||
|
||||
use super::request::ReceiptsRoom;
|
||||
|
||||
#[test]
|
||||
fn serialize_request_receipts_room() {
|
||||
let entry = ReceiptsRoom::AllSubscribed;
|
||||
assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#);
|
||||
|
||||
let entry = ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz"));
|
||||
assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_request_receipts_room() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ReceiptsRoom>(r#""*""#).unwrap(),
|
||||
ReceiptsRoom::AllSubscribed
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ReceiptsRoom>(r#""!foo:bar.baz""#).unwrap(),
|
||||
ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz"))
|
||||
);
|
||||
}
|
||||
}
|
@ -50,3 +50,6 @@ tracing = { version = "0.1.30", default-features = false, features = ["std"] }
|
||||
[dev-dependencies]
|
||||
ruma-client-api = { workspace = true, features = ["client"] }
|
||||
tokio-stream = "0.1.8"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -94,3 +94,6 @@ assert_matches2 = { workspace = true }
|
||||
assign = { workspace = true }
|
||||
maplit = { workspace = true }
|
||||
trybuild = "1.0.71"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -1,10 +0,0 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.pass("tests/api/ui/api-sanity-check.rs");
|
||||
t.pass("tests/api/ui/move-value.rs");
|
||||
t.pass("tests/api/ui/request-only.rs");
|
||||
t.pass("tests/api/ui/response-only.rs");
|
||||
t.compile_fail("tests/api/ui/deprecated-without-added.rs");
|
||||
t.compile_fail("tests/api/ui/removed-without-deprecated.rs");
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.pass("tests/identifiers/ui/01-valid-id-macros.rs");
|
||||
t.compile_fail("tests/identifiers/ui/02-invalid-id-macros.rs");
|
||||
t.compile_fail("tests/identifiers/ui/03-invalid-new-id-macros.rs");
|
||||
}
|
10
crates/ruma-common/tests/it/api/ruma_api.rs
Normal file
10
crates/ruma-common/tests/it/api/ruma_api.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.pass("tests/it/api/ui/api-sanity-check.rs");
|
||||
t.pass("tests/it/api/ui/move-value.rs");
|
||||
t.pass("tests/it/api/ui/request-only.rs");
|
||||
t.pass("tests/it/api/ui/response-only.rs");
|
||||
t.compile_fail("tests/it/api/ui/deprecated-without-added.rs");
|
||||
t.compile_fail("tests/it/api/ui/removed-without-deprecated.rs");
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
error: no rules expected the token `deprecated`
|
||||
--> tests/api/ui/deprecated-without-added.rs:9:16
|
||||
--> tests/it/api/ui/deprecated-without-added.rs:9:16
|
||||
|
|
||||
9 | 1.1 => deprecated,
|
||||
| ^^^^^^^^^^ no rules expected this token in macro call
|
@ -1,5 +1,5 @@
|
||||
error: no rules expected the token `removed`
|
||||
--> tests/api/ui/removed-without-deprecated.rs:9:16
|
||||
--> tests/it/api/ui/removed-without-deprecated.rs:9:16
|
||||
|
|
||||
9 | 1.1 => removed,
|
||||
| ^^^^^^^ no rules expected this token in macro call
|
7
crates/ruma-common/tests/it/identifiers/id_macros.rs
Normal file
7
crates/ruma-common/tests/it/identifiers/id_macros.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.pass("tests/it/identifiers/ui/01-valid-id-macros.rs");
|
||||
t.compile_fail("tests/it/identifiers/ui/02-invalid-id-macros.rs");
|
||||
t.compile_fail("tests/it/identifiers/ui/03-invalid-new-id-macros.rs");
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
error[E0080]: evaluation of constant value failed
|
||||
--> tests/identifiers/ui/03-invalid-new-id-macros.rs:2:13
|
||||
--> tests/it/identifiers/ui/03-invalid-new-id-macros.rs:2:13
|
||||
|
|
||||
2 | let _ = ruma_common::session_id!("invalid~");
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'Invalid Session ID: contains invalid characters', $DIR/tests/identifiers/ui/03-invalid-new-id-macros.rs:2:13
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'Invalid Session ID: contains invalid characters', $DIR/tests/it/identifiers/ui/03-invalid-new-id-macros.rs:2:13
|
||||
|
|
||||
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `ruma_common::session_id` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
note: erroneous constant encountered
|
||||
--> tests/identifiers/ui/03-invalid-new-id-macros.rs:2:13
|
||||
--> tests/it/identifiers/ui/03-invalid-new-id-macros.rs:2:13
|
||||
|
|
||||
2 | let _ = ruma_common::session_id!("invalid~");
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
@ -93,3 +93,6 @@ trybuild = "1.0.71"
|
||||
name = "event_deserialize"
|
||||
harness = false
|
||||
required-features = ["criterion"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -6,9 +6,11 @@
|
||||
|
||||
mod focus;
|
||||
mod member_data;
|
||||
mod member_state_key;
|
||||
|
||||
pub use focus::*;
|
||||
pub use member_data::*;
|
||||
pub use member_state_key::*;
|
||||
use ruma_common::MilliSecondsSinceUnixEpoch;
|
||||
use ruma_macros::{EventContent, StringEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -29,7 +31,7 @@ use crate::{
|
||||
///
|
||||
/// This struct also exposes allows to call the methods from [`CallMemberEventContent`].
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)]
|
||||
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = String, custom_redacted, custom_possibly_redacted)]
|
||||
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = CallMemberStateKey, custom_redacted, custom_possibly_redacted)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[serde(untagged)]
|
||||
pub enum CallMemberEventContent {
|
||||
@ -177,7 +179,7 @@ impl RedactContent for CallMemberEventContent {
|
||||
pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
|
||||
|
||||
impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
|
||||
type StateKey = String;
|
||||
type StateKey = CallMemberStateKey;
|
||||
}
|
||||
|
||||
/// The Redacted version of [`CallMemberEventContent`].
|
||||
@ -193,7 +195,7 @@ impl ruma_events::content::EventContent for RedactedCallMemberEventContent {
|
||||
}
|
||||
|
||||
impl RedactedStateEventContent for RedactedCallMemberEventContent {
|
||||
type StateKey = String;
|
||||
type StateKey = CallMemberStateKey;
|
||||
}
|
||||
|
||||
/// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
|
||||
@ -231,8 +233,11 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use assert_matches2::assert_matches;
|
||||
use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId};
|
||||
use serde_json::{from_value as from_json_value, json};
|
||||
use ruma_common::{
|
||||
device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId,
|
||||
OwnedUserId,
|
||||
};
|
||||
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
|
||||
|
||||
use super::{
|
||||
focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
|
||||
@ -467,8 +472,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn deserialize_member_event_helper(state_key: &str) {
|
||||
let ev = json!({
|
||||
fn member_event_json(state_key: &str) -> JsonValue {
|
||||
json!({
|
||||
"content":{
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
@ -497,7 +502,11 @@ mod tests {
|
||||
"prev_content": {},
|
||||
"prev_sender":"@user:example.org",
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_member_event_helper(state_key: &str) {
|
||||
let ev = member_event_json(state_key);
|
||||
|
||||
assert_matches!(
|
||||
from_json_value(ev),
|
||||
@ -507,7 +516,7 @@ mod tests {
|
||||
let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
|
||||
let sender = OwnedUserId::try_from("@user:example.org").unwrap();
|
||||
let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
|
||||
assert_eq!(member_event.state_key, state_key);
|
||||
assert_eq!(member_event.state_key.as_ref(), state_key);
|
||||
assert_eq!(member_event.event_id, event_id);
|
||||
assert_eq!(member_event.sender, sender);
|
||||
assert_eq!(member_event.room_id, room_id);
|
||||
@ -555,12 +564,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn deserialize_member_event_with_scoped_state_key_prefixed() {
|
||||
deserialize_member_event_helper("_@user:example.org:THIS_DEVICE");
|
||||
deserialize_member_event_helper("_@user:example.org_THIS_DEVICE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_member_event_with_scoped_state_key_unprefixed() {
|
||||
deserialize_member_event_helper("@user:example.org:THIS_DEVICE");
|
||||
deserialize_member_event_helper("@user:example.org_THIS_DEVICE");
|
||||
}
|
||||
|
||||
fn timestamps() -> (TS, TS, TS) {
|
||||
@ -632,4 +641,52 @@ mod tests {
|
||||
vec![] as Vec<MembershipData<'_>>
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rtc_member_event_key() {
|
||||
assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err());
|
||||
assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err());
|
||||
assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err());
|
||||
assert!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err()
|
||||
);
|
||||
|
||||
let user_id = user_id!("@username:example.org").as_str();
|
||||
let device_id = device_id!("VALID_DEVICE_ID").as_str();
|
||||
|
||||
let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id));
|
||||
assert_matches!(parse_result, Ok(_));
|
||||
assert_matches!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))),
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json(&format!(
|
||||
"{user_id}:invalid_suffix"
|
||||
))),
|
||||
Err(_)
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))),
|
||||
Err(_)
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))),
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json(&format!(
|
||||
"_{user_id}:invalid_suffix"
|
||||
))),
|
||||
Err(_)
|
||||
);
|
||||
assert_matches!(
|
||||
from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))),
|
||||
Err(_)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
225
crates/ruma-events/src/call/member/member_state_key.rs
Normal file
225
crates/ruma-events/src/call/member/member_state_key.rs
Normal file
@ -0,0 +1,225 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruma_common::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
||||
use serde::{
|
||||
de::{self, Deserialize, Deserializer, Unexpected},
|
||||
Serialize, Serializer,
|
||||
};
|
||||
/// A type that can be used as the `state_key` for call member state events.
|
||||
/// Those state keys can be a combination of UserId and DeviceId.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
pub struct CallMemberStateKey {
|
||||
key: CallMemberStateKeyEnum,
|
||||
raw: Box<str>,
|
||||
}
|
||||
|
||||
impl CallMemberStateKey {
|
||||
/// Constructs a new CallMemberStateKey there are three possible formats:
|
||||
/// - "_{UserId}_{DeviceId}" example: "_@test:user.org_DEVICE". `device_id`: Some`, `underscore:
|
||||
/// true`
|
||||
/// - "{UserId}_{DeviceId}" example: "@test:user.org_DEVICE". `device_id`: Some`, `underscore:
|
||||
/// false`
|
||||
/// - "{UserId}" example example: "@test:user.org". `device_id`: None`, underscore is ignored:
|
||||
/// `underscore: false|true`
|
||||
///
|
||||
/// Dependent on the parameters the correct CallMemberStateKey will be constructed.
|
||||
pub fn new(user_id: OwnedUserId, device_id: Option<OwnedDeviceId>, underscore: bool) -> Self {
|
||||
CallMemberStateKeyEnum::new(user_id, device_id, underscore).into()
|
||||
}
|
||||
|
||||
/// Returns the user id in this state key.
|
||||
/// (This is a cheap operations. The id is already type checked on initialization. And does
|
||||
/// only returns a reference to an existing OwnedUserId.)
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
match &self.key {
|
||||
CallMemberStateKeyEnum::UnderscoreUserDevice(u, _) => u,
|
||||
CallMemberStateKeyEnum::UserDevice(u, _) => u,
|
||||
CallMemberStateKeyEnum::User(u) => u,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the device id in this state key (if available)
|
||||
/// (This is a cheap operations. The id is already type checked on initialization. And does
|
||||
/// only returns a reference to an existing OwnedDeviceId.)
|
||||
pub fn device_id(&self) -> Option<&DeviceId> {
|
||||
match &self.key {
|
||||
CallMemberStateKeyEnum::UnderscoreUserDevice(_, d) => Some(d),
|
||||
CallMemberStateKeyEnum::UserDevice(_, d) => Some(d),
|
||||
CallMemberStateKeyEnum::User(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for CallMemberStateKey {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallMemberStateKeyEnum> for CallMemberStateKey {
|
||||
fn from(value: CallMemberStateKeyEnum) -> Self {
|
||||
let raw = value.to_string().into();
|
||||
Self { key: value, raw }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CallMemberStateKey {
|
||||
type Err = KeyParseError;
|
||||
|
||||
fn from_str(state_key: &str) -> Result<Self, Self::Err> {
|
||||
// Intentionally do not use CallMemberStateKeyEnum.into since this would reconstruct the
|
||||
// state key string.
|
||||
Ok(Self { key: CallMemberStateKeyEnum::from_str(state_key)?, raw: state_key.into() })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CallMemberStateKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = ruma_common::serde::deserialize_cow_str(deserializer)?;
|
||||
Self::from_str(&s).map_err(|err| de::Error::invalid_value(Unexpected::Str(&s), &err))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for CallMemberStateKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum represents all possible formats for a call member event state key.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum CallMemberStateKeyEnum {
|
||||
UnderscoreUserDevice(OwnedUserId, OwnedDeviceId),
|
||||
UserDevice(OwnedUserId, OwnedDeviceId),
|
||||
User(OwnedUserId),
|
||||
}
|
||||
|
||||
impl CallMemberStateKeyEnum {
|
||||
fn new(user_id: OwnedUserId, device_id: Option<OwnedDeviceId>, underscore: bool) -> Self {
|
||||
match (device_id, underscore) {
|
||||
(Some(device_id), true) => {
|
||||
CallMemberStateKeyEnum::UnderscoreUserDevice(user_id, device_id)
|
||||
}
|
||||
(Some(device_id), false) => CallMemberStateKeyEnum::UserDevice(user_id, device_id),
|
||||
(None, _) => CallMemberStateKeyEnum::User(user_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CallMemberStateKeyEnum {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
CallMemberStateKeyEnum::UnderscoreUserDevice(u, d) => write!(f, "_{u}_{d}"),
|
||||
CallMemberStateKeyEnum::UserDevice(u, d) => write!(f, "{u}_{d}"),
|
||||
CallMemberStateKeyEnum::User(u) => f.write_str(u.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CallMemberStateKeyEnum {
|
||||
type Err = KeyParseError;
|
||||
|
||||
fn from_str(state_key: &str) -> Result<Self, Self::Err> {
|
||||
// Ignore leading underscore if present
|
||||
// (used for avoiding auth rules on @-prefixed state keys)
|
||||
let (state_key, underscore) = match state_key.strip_prefix('_') {
|
||||
Some(s) => (s, true),
|
||||
None => (state_key, false),
|
||||
};
|
||||
|
||||
// Fail early if we cannot find the index of the ":"
|
||||
let Some(colon_idx) = state_key.find(':') else {
|
||||
return Err(KeyParseError::InvalidUser {
|
||||
user_id: state_key.to_owned(),
|
||||
error: ruma_common::IdParseError::MissingColon,
|
||||
});
|
||||
};
|
||||
|
||||
let (user_id, device_id) = match state_key[colon_idx + 1..].find('_') {
|
||||
None => {
|
||||
return match UserId::parse(state_key) {
|
||||
Ok(user_id) => {
|
||||
if underscore {
|
||||
Err(KeyParseError::LeadingUnderscoreNoDevice)
|
||||
} else {
|
||||
Ok(CallMemberStateKeyEnum::new(user_id, None, underscore))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(KeyParseError::InvalidUser {
|
||||
error: err,
|
||||
user_id: state_key.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
Some(suffix_idx) => {
|
||||
(&state_key[..colon_idx + 1 + suffix_idx], &state_key[colon_idx + 2 + suffix_idx..])
|
||||
}
|
||||
};
|
||||
|
||||
match (UserId::parse(user_id), OwnedDeviceId::from(device_id)) {
|
||||
(Ok(user_id), device_id) => {
|
||||
if device_id.as_str().is_empty() {
|
||||
return Err(KeyParseError::EmptyDevice);
|
||||
};
|
||||
Ok(CallMemberStateKeyEnum::new(user_id, Some(device_id), underscore))
|
||||
}
|
||||
(Err(err), _) => {
|
||||
Err(KeyParseError::InvalidUser { user_id: user_id.to_owned(), error: err })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error when trying to parse a call member state key.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum KeyParseError {
|
||||
/// The user part of the state key is invalid.
|
||||
#[error("uses a malformatted UserId in the UserId defined section.")]
|
||||
InvalidUser {
|
||||
/// The user Id that the parser thinks it should have parsed.
|
||||
user_id: String,
|
||||
/// The user Id parse error why if failed to parse it.
|
||||
error: ruma_common::IdParseError,
|
||||
},
|
||||
/// Uses a leading underscore but no trailing device id. The part after the underscore is a
|
||||
/// valid user id.
|
||||
#[error("uses a leading underscore but no trailing device id. The part after the underscore is a valid user id.")]
|
||||
LeadingUnderscoreNoDevice,
|
||||
/// Uses an empty device id. (UserId with trailing underscore)
|
||||
#[error("uses an empty device id. (UserId with trailing underscore)")]
|
||||
EmptyDevice,
|
||||
}
|
||||
|
||||
impl de::Expected for KeyParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "correct call member event key format. The provided string, {})", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::call::member::{member_state_key::CallMemberStateKeyEnum, CallMemberStateKey};
|
||||
|
||||
#[test]
|
||||
fn convert_state_key_enum_to_state_key() {
|
||||
let key = "_@user:domain.org_DEVICE";
|
||||
let state_key_enum = CallMemberStateKeyEnum::from_str(key).unwrap();
|
||||
// This generates state_key.raw from the enum
|
||||
let state_key: CallMemberStateKey = state_key_enum.into();
|
||||
// This compares state_key.raw (generated) with key (original)
|
||||
assert_eq!(state_key.as_ref(), key);
|
||||
// Compare to the from string without `CallMemberStateKeyEnum` step.
|
||||
let state_key_direct = CallMemberStateKey::from_str(state_key.as_ref()).unwrap();
|
||||
assert_eq!(state_key, state_key_direct);
|
||||
}
|
||||
}
|
@ -45,3 +45,6 @@ serde_json = { workspace = true }
|
||||
[dev-dependencies]
|
||||
assert_matches2 = { workspace = true }
|
||||
http = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -26,3 +26,6 @@ wildmatch = "2.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -25,3 +25,6 @@ compat-user-id = []
|
||||
[dependencies]
|
||||
js_int = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -25,3 +25,6 @@ serde = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -30,3 +30,6 @@ ruma-identifiers-validation = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
syn = { version = "2.0.2", features = ["extra-traits", "full", "visit"] }
|
||||
toml = { version = "0.8.2", default-features = false, features = ["parse"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -25,3 +25,6 @@ ruma-common = { workspace = true, features = ["api"] }
|
||||
ruma-events = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -24,3 +24,6 @@ tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.3.16"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -33,3 +33,6 @@ thiserror = { workspace = true }
|
||||
[dev-dependencies]
|
||||
assert_matches2 = { workspace = true }
|
||||
insta = "1.31.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -40,3 +40,6 @@ tracing-subscriber = "0.3.16"
|
||||
name = "state_res_bench"
|
||||
harness = false
|
||||
required-features = ["criterion"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -269,6 +269,7 @@ unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"]
|
||||
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
|
||||
unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"]
|
||||
unstable-msc4140 = ["ruma-client-api?/unstable-msc4140"]
|
||||
unstable-msc4186 = ["ruma-client-api?/unstable-msc4186"]
|
||||
unstable-pdu = ["ruma-events?/unstable-pdu"]
|
||||
unstable-unspecified = [
|
||||
"ruma-common/unstable-unspecified",
|
||||
@ -320,6 +321,7 @@ __unstable-mscs = [
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4186",
|
||||
]
|
||||
__ci = [
|
||||
"full",
|
||||
@ -352,3 +354,6 @@ ruma-push-gateway-api = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -1,4 +1 @@
|
||||
# Ruma Examples
|
||||
|
||||
These are example projects showcasing how to use the various crates in this repository. You can use
|
||||
these as a base for starting your own project.
|
||||
The examples have moved to another repository, at <https://github.com/ruma/examples>.
|
||||
|
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "hello_world"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
tokio = { version = "1.0.1", features = ["macros", "rt"] }
|
@ -1,13 +0,0 @@
|
||||
A simple example to demonstrate `ruma-client` functionality. Sends "Hello
|
||||
World!" to the given room.
|
||||
|
||||
# Usage
|
||||
|
||||
You will need to use an existing account on a homeserver that allows login with
|
||||
a password.
|
||||
|
||||
In this folder, you can run it with this command:
|
||||
|
||||
```shell
|
||||
cargo run <homeserver_url> <username> <password> <room>
|
||||
```
|
@ -1,49 +0,0 @@
|
||||
use std::{env, process::exit};
|
||||
|
||||
use ruma::{
|
||||
api::client::{alias::get_alias, membership::join_room_by_id, message::send_message_event},
|
||||
events::room::message::RoomMessageEventContent,
|
||||
OwnedRoomAliasId, TransactionId,
|
||||
};
|
||||
|
||||
type HttpClient = ruma::client::http_client::HyperNativeTls;
|
||||
|
||||
async fn hello_world(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
room_alias: OwnedRoomAliasId,
|
||||
) -> anyhow::Result<()> {
|
||||
let client =
|
||||
ruma::Client::builder().homeserver_url(homeserver_url).build::<HttpClient>().await?;
|
||||
client.log_in(username, password, None, Some("ruma-example-client")).await?;
|
||||
|
||||
let room_id = client.send_request(get_alias::v3::Request::new(room_alias)).await?.room_id;
|
||||
client.send_request(join_room_by_id::v3::Request::new(room_id.clone())).await?;
|
||||
client
|
||||
.send_request(send_message_event::v3::Request::new(
|
||||
room_id,
|
||||
TransactionId::new(),
|
||||
&RoomMessageEventContent::text_plain("Hello World!"),
|
||||
)?)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (homeserver_url, username, password, room) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3), env::args().nth(4)) {
|
||||
(Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password> <room>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
hello_world(homeserver_url, &username, &password, room.try_into()?).await
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "joke_bot"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
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.
|
||||
# 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"] }
|
||||
|
||||
futures-util = { version = "0.3.21", default-features = false, features = ["std"] }
|
||||
http = "1.1.0"
|
||||
http-body-util = "0.1.1"
|
||||
hyper = "1.3.1"
|
||||
hyper-tls = "0.6.0"
|
||||
hyper-util = { version = "0.1.3", features = ["client-legacy", "http1", "http2", "tokio"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1.7"
|
@ -1,30 +0,0 @@
|
||||
A simple bot to demonstrate `ruma-client` functionality. Tells jokes when you ask for them.
|
||||
|
||||
# Note on dependency versions
|
||||
|
||||
This example was written against pre-release versions of `ruma` and
|
||||
`ruma-client-api`. Check the comments in the `[dependencies]` section of
|
||||
[`Cargo.toml`](Cargo.toml) for more information.
|
||||
|
||||
# Usage
|
||||
|
||||
Create a file called `config` and populate it with the following values in `key=value` format:
|
||||
|
||||
- `homeserver`: Your homeserver URL.
|
||||
- `username`: The Matrix ID for the bot.
|
||||
- `password`: The password for the bot.
|
||||
|
||||
For example:
|
||||
|
||||
```ini
|
||||
homeserver=https://example.com:8448/
|
||||
username=@user:example.com
|
||||
password=yourpassword
|
||||
```
|
||||
|
||||
You will need to pre-register the bot account; it doesn't do registration
|
||||
automatically. The bot will automatically join rooms it is invited to though.
|
||||
|
||||
Finally, run the bot (e.g. using `cargo run`) from the same directory as your
|
||||
`config` file. The bot should respond to the request "Tell me a joke" in any
|
||||
channel that it is invited to.
|
@ -1,278 +0,0 @@
|
||||
use std::{error::Error, io, process::exit, time::Duration};
|
||||
|
||||
use futures_util::future::{join, join_all};
|
||||
use http_body_util::BodyExt as _;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
filter::FilterDefinition, membership::join_room_by_id, message::send_message_event,
|
||||
sync::sync_events,
|
||||
},
|
||||
assign, client,
|
||||
events::{
|
||||
room::message::{MessageType, RoomMessageEventContent},
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent,
|
||||
},
|
||||
presence::PresenceState,
|
||||
serde::Raw,
|
||||
OwnedRoomId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
use tokio::fs;
|
||||
use tokio_stream::StreamExt as _;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
if let Err(e) = run().await {
|
||||
eprintln!("{e}");
|
||||
exit(1)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type HttpClient = client::http_client::HyperNativeTls;
|
||||
type MatrixClient = client::Client<client::http_client::HyperNativeTls>;
|
||||
|
||||
async fn run() -> Result<(), Box<dyn Error>> {
|
||||
let config =
|
||||
read_config().await.map_err(|e| format!("configuration in ./config is invalid: {e}"))?;
|
||||
let http_client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
|
||||
.build(hyper_tls::HttpsConnector::new());
|
||||
let matrix_client = if let Some(state) = read_state().await.ok().flatten() {
|
||||
ruma::Client::builder()
|
||||
.homeserver_url(config.homeserver.clone())
|
||||
.access_token(Some(state.access_token))
|
||||
.http_client(http_client.clone())
|
||||
.await?
|
||||
} else if config.password.is_some() {
|
||||
let client = create_matrix_session(http_client.clone(), &config).await?;
|
||||
|
||||
if let Err(err) = write_state(&State {
|
||||
access_token: client.access_token().expect("Matrix access token is missing"),
|
||||
})
|
||||
.await
|
||||
{
|
||||
eprintln!(
|
||||
"Failed to persist access token to disk. \
|
||||
Re-authentication will be required on the next startup: {err}",
|
||||
);
|
||||
}
|
||||
client
|
||||
} else {
|
||||
return Err("No previous session found and no credentials stored in config".into());
|
||||
};
|
||||
|
||||
let filter = FilterDefinition::ignore_all().into();
|
||||
let initial_sync_response = matrix_client
|
||||
.send_request(assign!(sync_events::v3::Request::new(), {
|
||||
filter: Some(filter),
|
||||
}))
|
||||
.await?;
|
||||
let user_id = &config.username;
|
||||
let not_senders = vec![user_id.clone()];
|
||||
let filter = {
|
||||
let mut filter = FilterDefinition::empty();
|
||||
filter.room.timeline.not_senders = not_senders;
|
||||
filter
|
||||
}
|
||||
.into();
|
||||
|
||||
let mut sync_stream = Box::pin(matrix_client.sync(
|
||||
Some(filter),
|
||||
initial_sync_response.next_batch,
|
||||
PresenceState::Online,
|
||||
Some(Duration::from_secs(30)),
|
||||
));
|
||||
|
||||
// Prevent the clients being moved by `async move` blocks
|
||||
let http_client = &http_client;
|
||||
let matrix_client = &matrix_client;
|
||||
|
||||
println!("Listening...");
|
||||
while let Some(response) = sync_stream.try_next().await? {
|
||||
let message_futures = response.rooms.join.iter().map(|(room_id, room_info)| async move {
|
||||
// Use a regular for loop for the messages within one room to handle them sequentially
|
||||
for e in &room_info.timeline.events {
|
||||
if let Err(err) =
|
||||
handle_message(http_client, matrix_client, e, room_id.to_owned(), user_id).await
|
||||
{
|
||||
eprintln!("failed to respond to message: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let invite_futures = response.rooms.invite.into_keys().map(|room_id| async move {
|
||||
if let Err(err) = handle_invitations(http_client, matrix_client, room_id.clone()).await
|
||||
{
|
||||
eprintln!("failed to accept invitation for room {room_id}: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle messages from different rooms as well as invites concurrently
|
||||
join(join_all(message_futures), join_all(invite_futures)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_matrix_session(
|
||||
http_client: HttpClient,
|
||||
config: &Config,
|
||||
) -> Result<MatrixClient, Box<dyn Error>> {
|
||||
if let Some(password) = &config.password {
|
||||
let client = ruma::Client::builder()
|
||||
.homeserver_url(config.homeserver.clone())
|
||||
.http_client(http_client)
|
||||
.await?;
|
||||
|
||||
if let Err(e) = client.log_in(config.username.as_ref(), password, None, None).await {
|
||||
let reason = match e {
|
||||
client::Error::AuthenticationRequired => "invalid credentials specified".to_owned(),
|
||||
client::Error::Response(response_err) => {
|
||||
format!("failed to get a response from the server: {response_err}")
|
||||
}
|
||||
client::Error::FromHttpResponse(parse_err) => {
|
||||
format!("failed to parse log in response: {parse_err}")
|
||||
}
|
||||
_ => e.to_string(),
|
||||
};
|
||||
return Err(format!("Failed to log in: {reason}").into());
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
} else {
|
||||
Err("Failed to create session: no password stored in config".to_owned().into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
http_client: &HttpClient,
|
||||
matrix_client: &MatrixClient,
|
||||
e: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: OwnedRoomId,
|
||||
bot_user_id: &UserId,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncMessageLikeEvent::Original(m),
|
||||
))) = e.deserialize()
|
||||
{
|
||||
// workaround because Conduit does not implement filtering.
|
||||
if m.sender == bot_user_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let MessageType::Text(t) = m.content.msgtype {
|
||||
println!("{}:\t{}", m.sender, t.body);
|
||||
if t.body.to_ascii_lowercase().contains("joke") {
|
||||
let joke = match get_joke(http_client).await {
|
||||
Ok(joke) => joke,
|
||||
Err(_) => "I thought of a joke... but I just forgot it.".to_owned(),
|
||||
};
|
||||
let joke_content = RoomMessageEventContent::text_plain(joke);
|
||||
|
||||
let txn_id = TransactionId::new();
|
||||
let req = send_message_event::v3::Request::new(
|
||||
room_id.to_owned(),
|
||||
txn_id,
|
||||
&joke_content,
|
||||
)?;
|
||||
// Do nothing if we can't send the message.
|
||||
let _ = matrix_client.send_request(req).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_invitations(
|
||||
http_client: &HttpClient,
|
||||
matrix_client: &MatrixClient,
|
||||
room_id: OwnedRoomId,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
println!("invited to {room_id}");
|
||||
matrix_client.send_request(join_room_by_id::v3::Request::new(room_id.clone())).await?;
|
||||
|
||||
let greeting = "Hello! My name is Mr. Bot! I like to tell jokes. Like this one: ";
|
||||
let joke = get_joke(http_client).await.unwrap_or_else(|_| "err... never mind.".to_owned());
|
||||
let content = RoomMessageEventContent::text_plain(format!("{greeting}\n{joke}"));
|
||||
let txn_id = TransactionId::new();
|
||||
let message = send_message_event::v3::Request::new(room_id, txn_id, &content)?;
|
||||
matrix_client.send_request(message).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_joke(client: &HttpClient) -> Result<String, Box<dyn Error>> {
|
||||
let uri = "https://v2.jokeapi.dev/joke/Programming,Pun,Misc?safe-mode&type=single"
|
||||
.parse::<hyper::Uri>()?;
|
||||
let rsp = client.get(uri).await?;
|
||||
let bytes = rsp.into_body().collect().await?.to_bytes();
|
||||
let joke_obj = serde_json::from_slice::<JsonValue>(&bytes)
|
||||
.map_err(|_| "invalid JSON returned from joke API")?;
|
||||
let joke = joke_obj["joke"].as_str().ok_or("joke field missing from joke API response")?;
|
||||
Ok(joke.to_owned())
|
||||
}
|
||||
|
||||
struct State {
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
async fn write_state(state: &State) -> io::Result<()> {
|
||||
let content = &state.access_token;
|
||||
fs::write("./session", content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_state() -> io::Result<Option<State>> {
|
||||
match fs::read_to_string("./session").await {
|
||||
Ok(access_token) => Ok(Some(State { access_token })),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
struct Config {
|
||||
homeserver: String,
|
||||
username: OwnedUserId,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
async fn read_config() -> io::Result<Config> {
|
||||
let content = fs::read_to_string("./config").await?;
|
||||
let lines = content.split('\n');
|
||||
|
||||
let mut homeserver = None;
|
||||
let mut username = Err("required field `username` is missing".to_owned());
|
||||
let mut password = None;
|
||||
for line in lines {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
match key.trim() {
|
||||
"homeserver" => homeserver = Some(value.trim().to_owned()),
|
||||
// TODO: infer domain from `homeserver`
|
||||
"username" => {
|
||||
username =
|
||||
value.trim().to_owned().try_into().map_err(|e| {
|
||||
format!("invalid Matrix user ID format for `username`: {e}")
|
||||
});
|
||||
}
|
||||
"password" => password = Some(value.trim().to_owned()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (homeserver, username) {
|
||||
(Some(homeserver), Ok(username)) => Ok(Config { homeserver, username, password }),
|
||||
(homeserver, username) => {
|
||||
let mut error = String::from("Invalid config specified:");
|
||||
if homeserver.is_none() {
|
||||
error.push_str("\n required field `homeserver` is missing");
|
||||
}
|
||||
if let Err(e) = username {
|
||||
error.push_str("\n ");
|
||||
error.push_str(&e);
|
||||
}
|
||||
Err(io::Error::new(io::ErrorKind::InvalidData, error))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "message_log"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.37"
|
||||
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-stream = { version = "0.1.1", default-features = false }
|
@ -1,13 +0,0 @@
|
||||
A simple example to demonstrate `ruma-client` functionality. Prints all the
|
||||
received text messages to stdout.
|
||||
|
||||
# Usage
|
||||
|
||||
You will need to use an existing account on a homeserver that allows login with
|
||||
a password.
|
||||
|
||||
In this folder, you can run it with this command:
|
||||
|
||||
```shell
|
||||
cargo run <homeserver_url> <username> <password>
|
||||
```
|
@ -1,85 +0,0 @@
|
||||
use std::{env, process::exit, time::Duration};
|
||||
|
||||
use ruma::{
|
||||
api::client::{filter::FilterDefinition, sync::sync_events},
|
||||
assign,
|
||||
events::{
|
||||
room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
presence::PresenceState,
|
||||
};
|
||||
use tokio_stream::StreamExt as _;
|
||||
|
||||
type HttpClient = ruma::client::http_client::HyperNativeTls;
|
||||
|
||||
async fn log_messages(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let client =
|
||||
ruma::Client::builder().homeserver_url(homeserver_url).build::<HttpClient>().await?;
|
||||
|
||||
client.log_in(username, password, None, None).await?;
|
||||
|
||||
let filter = FilterDefinition::ignore_all().into();
|
||||
let initial_sync_response = client
|
||||
.send_request(assign!(sync_events::v3::Request::new(), {
|
||||
filter: Some(filter),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let mut sync_stream = Box::pin(client.sync(
|
||||
None,
|
||||
initial_sync_response.next_batch,
|
||||
PresenceState::Online,
|
||||
Some(Duration::from_secs(30)),
|
||||
));
|
||||
|
||||
while let Some(res) = sync_stream.try_next().await? {
|
||||
// Only look at rooms the user hasn't left yet
|
||||
for (room_id, room) in res.rooms.join {
|
||||
for event in room.timeline.events.into_iter().flat_map(|r| r.deserialize()) {
|
||||
// Filter out the text messages
|
||||
if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncMessageLikeEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content:
|
||||
RoomMessageEventContent {
|
||||
msgtype:
|
||||
MessageType::Text(TextMessageEventContent {
|
||||
body: msg_body, ..
|
||||
}),
|
||||
..
|
||||
},
|
||||
sender,
|
||||
..
|
||||
}),
|
||||
)) = event
|
||||
{
|
||||
println!("{sender} in {room_id}: {msg_body}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
log_messages(homeserver_url, &username, &password).await
|
||||
}
|
@ -17,3 +17,6 @@ serde_json = { workspace = true }
|
||||
toml = { version = "0.8.2", default-features = false, features = ["parse"] }
|
||||
toml_edit = { version = "0.20.2", optional = true }
|
||||
xshell = "0.1.17"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
Loading…
x
Reference in New Issue
Block a user