ruma-common-0.10.5/.cargo_vcs_info.json0000644000000001600000000000100133460ustar { "git": { "sha1": "67b2ec7d34eb35e47c7bf1d0da0e6326049179ac" }, "path_in_vcs": "crates/ruma-common" }ruma-common-0.10.5/CHANGELOG.md000064400000000000000000000202021046102023000137460ustar 00000000000000# [unreleased] # 0.10.5 Improvements: * Add support for `#[incoming_derive(!Debug)]` to the `Incoming` derive macro # 0.10.4 Bug fixes: * Fix `MatrixToUri` parsing for non-url-encoded room aliases # 0.10.3 Bug fixes: * Fix ruma-common not compiling with the Cargo features `events` and `unstable-msc2677` active, and `unstable-msc2676` inactive # 0.10.2 Improvements: * Add `relations` accessors to event enum types: * `AnyMessageLikeEvent` and `AnySyncMessageLikeEvent` * `AnyStateEvent` and `AnySyncStateEvent` * `AnyTimelineEvent` and `AnySyncTimelineEvent` # 0.10.1 Improvements: * Add `RoomMessageEventContent::make_reply_to` * Deprecate reply constructors in favor of the new method # 0.10.0 Bug fixes: * Expose `MatrixIdError`, `MatrixToError`, `MatrixUriError` and `MxcUriError` at the crate root * Fix matching of `event_match` condition * The spec clarified its behavior: Breaking changes: * Add `user_id` field to `PushConditionRoomCtx` * Remove `PartialEq` implementation on `NotificationPowerLevels` * Remove `PartialEq` implementation for `events::call::SessionDescription` * Use new `events::call::AnswerSessionDescription` for `CallAnswerEventContent` and `OfferSessionDescription` for `CallInviteEventContent` * Use new `VoipVersionId` and `VoipId` types for `events::call` events * Remove `RoomName` / `OwnedRoomName` and replace usages with `str` / `String` * Room name size limits were never enforced by servers ([Spec change removing the size limit][spec]) * Remove `RoomMessageFeedbackEvent` and associated types and variants according to MSC3582 * Move `CanonicalJson`, `CanonicalJsonObject` and `CanonicalJsonError` out of the `serde` module and behind the cargo feature flag `canonical-json` * Make identifiers matrix URI constructors generic over owned parameters * Split `RoomId` matrix URI constructors between methods with and without routing * Allow to add routing servers to `RoomId::matrix_to_event_uri()` * Move `receipt::ReceiptType` to `events::receipt` * Make `Clone` as supertrait of `api::OutgoingRequest` * Rename `Any[Sync]RoomEvent` to `Any[Sync]TimelineEvent` * `RoomMemberEvent` and related types now have a custom unsigned type including the `invite_room_state` field, instead of the `StateUnsigned` type used by other state events [spec]: https://github.com/matrix-org/matrix-spec-proposals/pull/3669 Improvements: * All push rules are now considered to not apply to events sent by the user themselves * Change `events::relation::BundledAnnotation` to a struct instead of an enum * Remove `BundledReaction` * Add unstable support for polls (MSC3381) * Add unstable support for Improved Signalling for 1:1 VoIP (MSC2746) * Add support for knocking in `events::room::member::MembershipChange` * Add `MatrixVersion::V1_3` * Deprecate the `sender_key` and `device_id` fields for encrypted events (MSC3700) * Move the `relations` field of `events::unsigned` types out of `unstable-msc2675` * Deserialize stringified integers for power levels without the `compat` feature * Add `JoinRule::KnockRestricted` (MSC3787) * Add `MatrixVersionId::V10` (MSC3604) * Add methods to sanitize messages according to the spec behind the `unstable-sanitize` feature * Can also remove rich reply fallbacks * Implement `From` for `identifiers::matrix_uri::MatrixId` * Add unstable default push rule to ignore room server ACLs events (MSC3786) * Add unstable support for private read receipts (MSC2285) * Add unstable support for filtering public rooms by room type (MSC3827) # 0.9.2 Bug fixes: * Fix serialization and deserialization of events with a dynamic `event_type` # 0.9.1 Improvements: * Add `StrippedPowerLevelsEvent::power_levels` * Add (`Sync`)`RoomMemberEvent::membership` * Export `events::room::member::Change` * Prior to this, you couldn't actually do anything with the `membership_change` functions on various member event types # 0.9.0 Bug fixes: * Change default `invite` power level to `0` * The spec was determined to be wrong about the default: Breaking changes: * Several ruma crates have been merged into `ruma-common` * `ruma-api` has moved into `api`, behind a feature flag * `ruma-events` has moved into `events`, behind a feature flag * `ruma-identifiers` types are available at the root of the crate * `ruma-serde` has moved into `serde` * The `events::*MessageEvent` types have been renamed to `*MessageLikeEvent` * Change `events::room` media types to accept either a plain file or an encrypted file, not both simultaneously * Change `events::room` media types to use `Duration` where applicable * Move `prev_content` into `unsigned` * Rename `identifiers::Error` to `IdParseError` * Fix the `RoomMessageEventContent::*_reply_plain` methods that now return a message with a `formatted_body`, according to the spec. Therefore, they only accept `OriginalRoomMessageEvent`s like their HTML counterparts. * Update the `state_key` field of state events to be of a different type depending on the content type. You now no longer need to validate manually that `m.room.member` events have a user ID as their state key! Improvements: * Add unstable support for extensible events (MSCs 1767, 3551, 3552, 3553, 3246, 3488) * Add unstable support for translatable text content (MSC3554) * Add unstable support for voice messages (MSC3245) * Add unstable support for threads (MSC3440) * Add `ReceiptEventContent::user_receipt` * Make `Restricted::allow` public * Conversion from `RoomPowerLevels` to `RoomPowerLevelsEventContent` # 0.8.0 Breaking changes: * Update `ruma-identifiers` dependency # 0.7.0 Breaking changes: * Update `ruma-identifiers` dependency * Use new `Base64` type for `key` field of `SignedKey` # 0.6.0 Breaking changes: * Make a few enums non-exhaustive * Upgrade dependencies # 0.5.4 Improvements: * Add `to_device` module containing `DeviceIdOrAllDevices` # 0.5.3 Improvements: * Add `instance_id` field to `ProtocolInstance[Init]` under the `unstable-pre-spec` feature # 0.5.2 Improvements: * Add `thirdparty::ThirdPartyIdentifier` # 0.5.1 Improvements: * Add `receipt::ReceiptType` * Add `MilliSecondsSinceUnixEpoch` and `SecondsSinceUnixEpoch` types * Bump dependency versions # 0.5.0 Breaking changes: * Rename `push::RulesetIter` to `push::RulesetIntoIter` * Change the return type of `push::Ruleset::get_actions` from an iterator to a slice Improvements: * Add `push::Ruleset::iter()` for borrowing iteration of rulesets * Add conversions between `AnyPushRule` and `AnyPushRuleRef` (`AnyPushRule::as_ref` and `AnyPushRuleRef::to_owned`) * Add `push::Ruleset::get_match()` for finding the first matching push rule for an event. This is pretty much the same thing as `get_actions()` but returns the entire push rule, not just its actions. # 0.4.0 Breaking changes: * Use `ruma_identifiers::MxcUri` instead of `String` for `avatar_url` field in `directory::PublicRoomsChunk` * Use `ruma_identifiers::RoomId` instead of `String` for `room_id` field in `push::PushConditionRoomCtx` * Upgrade ruma-identifiers dependency to 0.19.0 # 0.3.1 Bug fixes: * Fix `push::PushCondition::applies` for empty value and pattern # 0.3.0 Breaking changes: * Update set of conversion trait implementations for enums * Replace `Vec` by `IndexSet` in `push::Ruleset` * Replace `push::AnyPushRule` with an enum (the original struct still exists as just `PushRule` in `ruma-client-api`) * … (there's a lot more, but this changelog was not kept up to date; PRs to improve it are welcome) Improvements: * Add the `thirdparty` module * Add `directory::{Filter, PublicRoomsChunk, RoomNetwork}` (moved from `ruma_client_api::r0::directory`) * Add `push::{PusherData, PushFormat}` (moved from `ruma_client_api::r0::push`) * Add `authentication::TokenType` (moved from `ruma_client_api::r0::account:request_openid_token`) * Add an `IntoIterator` implementation for `Ruleset` * Add `push::Ruleset::get_actions` * Add `push::PushCondition::applies` * Add `push::{FlattenedJson, PushConditionRoomCtx}` # 0.2.0 Breaking changes: * Make most types defined by the crate `#[non_exhaustive]` ruma-common-0.10.5/Cargo.toml0000644000000075350000000000100113610ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.60" name = "ruma-common" version = "0.10.5" description = "Common types for other ruma crates." homepage = "https://www.ruma.io/" readme = "README.md" keywords = [ "matrix", "chat", "messaging", "ruma", ] license = "MIT" repository = "https://github.com/ruma/ruma" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [[bench]] name = "event_deserialize" harness = false required-features = ["criterion"] [dependencies.base64] version = "0.13.0" [dependencies.bytes] version = "1.0.1" [dependencies.criterion] version = "0.3.3" optional = true [dependencies.form_urlencoded] version = "1.0.0" [dependencies.getrandom] version = "0.2.6" optional = true [dependencies.html5ever] version = "0.25.2" optional = true [dependencies.http] version = "0.2.2" optional = true [dependencies.indexmap] version = "1.9.1" features = ["serde"] [dependencies.itoa] version = "1.0.1" [dependencies.js_int] version = "0.2.0" features = ["serde"] [dependencies.js_option] version = "0.1.0" [dependencies.percent-encoding] version = "2.1.0" [dependencies.phf] version = "0.10.1" features = ["macros"] optional = true [dependencies.pulldown-cmark] version = "0.9.1" optional = true default-features = false [dependencies.rand_crate] version = "0.8.3" optional = true package = "rand" [dependencies.regex] version = "1.5.6" features = [ "std", "perf", ] default-features = false [dependencies.ruma-identifiers-validation] version = "0.9.0" default-features = false [dependencies.ruma-macros] version = "0.10.5" [dependencies.serde] version = "1.0.118" features = ["derive"] [dependencies.serde_json] version = "1.0.64" features = ["raw_value"] [dependencies.thiserror] version = "1.0.26" optional = true [dependencies.tracing] version = "0.1.25" [dependencies.url] version = "2.2.2" [dependencies.uuid] version = "1.0.0" features = ["v4"] optional = true [dependencies.wildmatch] version = "2.0.0" [dev-dependencies.assert_matches] version = "1.5.0" [dev-dependencies.assign] version = "1.1.1" [dev-dependencies.http] version = "0.2.2" [dev-dependencies.maplit] version = "1.0.2" [dev-dependencies.trybuild] version = "1.0.42" [features] api = [ "dep:http", "dep:thiserror", ] canonical-json = [] client = [] compat = [ "ruma-macros/compat", "ruma-identifiers-validation/compat", ] default = [ "client", "server", ] events = ["dep:thiserror"] js = [ "dep:js-sys", "getrandom?/js", "uuid?/js", ] markdown = ["pulldown-cmark"] rand = [ "dep:rand_crate", "dep:uuid", ] server = [] unstable-exhaustive-types = [] unstable-msc1767 = [] unstable-msc2285 = [] unstable-msc2448 = [] unstable-msc2676 = [] unstable-msc2677 = [] unstable-msc2746 = [] unstable-msc2870 = [] unstable-msc3245 = ["unstable-msc3246"] unstable-msc3246 = [ "unstable-msc3551", "dep:thiserror", ] unstable-msc3381 = ["unstable-msc1767"] unstable-msc3440 = [] unstable-msc3488 = ["unstable-msc1767"] unstable-msc3551 = ["unstable-msc1767"] unstable-msc3552 = ["unstable-msc3551"] unstable-msc3553 = ["unstable-msc3552"] unstable-msc3554 = ["unstable-msc1767"] unstable-msc3786 = [] unstable-msc3827 = [] unstable-pdu = [] unstable-pre-spec = [] unstable-sanitize = [ "dep:html5ever", "dep:phf", ] [target."cfg(all(target_arch = \"wasm32\", target_os = \"unknown\"))".dependencies.js-sys] version = "0.3" optional = true ruma-common-0.10.5/Cargo.toml.orig000064400000000000000000000062451046102023000150370ustar 00000000000000[package] name = "ruma-common" version = "0.10.5" description = "Common types for other ruma crates." homepage = "https://www.ruma.io/" keywords = ["matrix", "chat", "messaging", "ruma"] license = "MIT" readme = "README.md" repository = "https://github.com/ruma/ruma" edition = "2021" rust-version = "1.60" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] # These feature gates exist only for the tests. Disabling them results in a # compile error. default = ["client", "server"] client = [] server = [] api = ["dep:http", "dep:thiserror"] canonical-json = [] compat = ["ruma-macros/compat", "ruma-identifiers-validation/compat"] events = ["dep:thiserror"] js = ["dep:js-sys", "getrandom?/js", "uuid?/js"] markdown = ["pulldown-cmark"] # Dependency should not be renamed, but if it isn't, trybuild breaks: # https://github.com/dtolnay/trybuild/issues/171 rand = ["dep:rand_crate", "dep:uuid"] unstable-exhaustive-types = [] unstable-pdu = [] unstable-pre-spec = [] unstable-sanitize = ["dep:html5ever", "dep:phf"] unstable-msc1767 = [] unstable-msc2285 = [] unstable-msc2448 = [] unstable-msc2676 = [] unstable-msc2677 = [] unstable-msc2746 = [] unstable-msc2870 = [] unstable-msc3245 = ["unstable-msc3246"] unstable-msc3246 = ["unstable-msc3551", "dep:thiserror"] unstable-msc3381 = ["unstable-msc1767"] unstable-msc3440 = [] unstable-msc3488 = ["unstable-msc1767"] unstable-msc3551 = ["unstable-msc1767"] unstable-msc3552 = ["unstable-msc3551"] unstable-msc3553 = ["unstable-msc3552"] unstable-msc3554 = ["unstable-msc1767"] unstable-msc3786 = [] unstable-msc3827 = [] [dependencies] base64 = "0.13.0" bytes = "1.0.1" form_urlencoded = "1.0.0" getrandom = { version = "0.2.6", optional = true } html5ever = { version = "0.25.2", optional = true } http = { version = "0.2.2", optional = true } indexmap = { version = "1.9.1", features = ["serde"] } itoa = "1.0.1" js_int = { version = "0.2.0", features = ["serde"] } js_option = "0.1.0" percent-encoding = "2.1.0" phf = { version = "0.10.1", features = ["macros"], optional = true } pulldown-cmark = { version = "0.9.1", default-features = false, optional = true } rand_crate = { package = "rand", version = "0.8.3", optional = true } regex = { version = "1.5.6", default-features = false, features = ["std", "perf"] } ruma-identifiers-validation = { version = "0.9.0", path = "../ruma-identifiers-validation", default-features = false } ruma-macros = { version = "0.10.5", path = "../ruma-macros" } serde = { version = "1.0.118", features = ["derive"] } serde_json = { version = "1.0.64", features = ["raw_value"] } thiserror = { version = "1.0.26", optional = true } tracing = "0.1.25" url = "2.2.2" uuid = { version = "1.0.0", optional = true, features = ["v4"] } wildmatch = "2.0.0" # dev-dependencies can't be optional, so this is a regular dependency criterion = { version = "0.3.3", optional = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] js-sys = { version = "0.3", optional = true } [dev-dependencies] assert_matches = "1.5.0" assign = "1.1.1" http = "0.2.2" maplit = "1.0.2" trybuild = "1.0.42" [[bench]] name = "event_deserialize" harness = false required-features = ["criterion"] ruma-common-0.10.5/README.md000064400000000000000000000014341046102023000134220ustar 00000000000000# ruma-common [![crates.io page](https://img.shields.io/crates/v/ruma-common.svg)](https://crates.io/crates/ruma-common) [![docs.rs page](https://docs.rs/ruma-common/badge.svg)](https://docs.rs/ruma-common/) ![license: MIT](https://img.shields.io/crates/l/ruma-common.svg) Common types for other Ruma crates. The feature-gated modules are defined as follow: ### `api` module Behind the `api` feature, core types used to define the requests and responses for each endpoint in the various [Matrix](https://matrix.org/) API specifications. These types can be shared by client and server code for all Matrix APIs. ### `events` module Behind the `events` feature, serializable types for the events in the [Matrix](https://matrix.org/) specification that can be shared by client and server code.ruma-common-0.10.5/benches/event_deserialize.rs000064400000000000000000000051451046102023000176240ustar 00000000000000// `cargo bench` works, but if you use `cargo bench -- --save-baseline ` // or pass any other args to it, it fails with the error // `cargo bench unknown option --save-baseline`. // To pass args to criterion, use this form // `cargo bench --features criterion --bench -- --save-baseline `. #![cfg(feature = "events")] #![allow(unused_imports, dead_code)] use criterion::{criterion_group, criterion_main, Criterion}; use ruma_common::{ events::{ room::power_levels::RoomPowerLevelsEventContent, AnyStateEvent, AnyTimelineEvent, OriginalStateEvent, }, serde::Raw, }; use serde_json::json; fn power_levels() -> serde_json::Value { json!({ "content": { "ban": 50, "events": { "m.room.avatar": 50, "m.room.canonical_alias": 50, "m.room.history_visibility": 100, "m.room.name": 50, "m.room.power_levels": 100 }, "events_default": 0, "invite": 0, "kick": 50, "redact": 50, "state_default": 50, "users": { "@example:localhost": 100 }, "users_default": 0 }, "event_id": "$15139375512JaHAW:localhost", "origin_server_ts": 45, "sender": "@example:localhost", "room_id": "!room:localhost", "state_key": "", "type": "m.room.power_levels", "unsigned": { "age": 45 } }) } fn deserialize_any_room_event(c: &mut Criterion) { let json_data = power_levels(); c.bench_function("deserialize to `AnyTimelineEvent`", |b| { b.iter(|| { let _ = serde_json::from_value::(json_data.clone()).unwrap(); }) }); } fn deserialize_any_state_event(c: &mut Criterion) { let json_data = power_levels(); c.bench_function("deserialize to `AnyStateEvent`", |b| { b.iter(|| { let _ = serde_json::from_value::(json_data.clone()).unwrap(); }) }); } fn deserialize_specific_event(c: &mut Criterion) { let json_data = power_levels(); c.bench_function("deserialize to `OriginalStateEvent`", |b| { b.iter(|| { let _ = serde_json::from_value::>( json_data.clone(), ) .unwrap(); }) }); } criterion_group!( benches, deserialize_any_room_event, deserialize_any_state_event, deserialize_specific_event ); criterion_main!(benches); ruma-common-0.10.5/src/api/error.rs000064400000000000000000000222001046102023000151740ustar 00000000000000//! This module contains types for all kinds of errors that can occur when //! converting between http requests / responses and ruma's representation of //! matrix API requests / responses. use std::{error::Error as StdError, fmt}; use bytes::BufMut; use serde_json::{from_slice as from_json_slice, Value as JsonValue}; use thiserror::Error; use super::{EndpointError, MatrixVersion, OutgoingResponse}; /// A general-purpose Matrix error type consisting of an HTTP status code and a JSON body. /// /// Note that individual `ruma-*-api` crates may provide more specific error types. #[allow(clippy::exhaustive_structs)] #[derive(Clone, Debug)] pub struct MatrixError { /// The http response's status code. pub status_code: http::StatusCode, /// The http response's body. pub body: JsonValue, } impl fmt::Display for MatrixError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[{}] ", self.status_code.as_u16())?; fmt::Display::fmt(&self.body, f) } } impl StdError for MatrixError {} impl OutgoingResponse for MatrixError { fn try_into_http_response( self, ) -> Result, IntoHttpError> { http::Response::builder() .header(http::header::CONTENT_TYPE, "application/json") .status(self.status_code) .body(crate::serde::json_to_buf(&self.body)?) .map_err(Into::into) } } impl EndpointError for MatrixError { fn try_from_http_response>( response: http::Response, ) -> Result { Ok(Self { status_code: response.status(), body: from_json_slice(response.body().as_ref())?, }) } } /// An error when converting one of ruma's endpoint-specific request or response /// types to the corresponding http type. #[derive(Debug, Error)] #[non_exhaustive] pub enum IntoHttpError { /// Tried to create an authentication request without an access token. #[error("no access token given, but this endpoint requires one")] NeedsAuthentication, /// Tried to create a request with an old enough version, for which no unstable endpoint /// exists. /// /// This is also a fallback error for if the version is too new for this endpoint. #[error( "endpoint was not supported by server-reported versions, \ but no unstable path to fall back to was defined" )] NoUnstablePath, /// Tried to create a request with [`MatrixVersion`]s for all of which this endpoint was /// removed. #[error("could not create any path variant for endpoint, as it was removed in version {0}")] EndpointRemoved(MatrixVersion), /// JSON serialization failed. #[error("JSON serialization failed: {0}")] Json(#[from] serde_json::Error), /// Query parameter serialization failed. #[error("query parameter serialization failed: {0}")] Query(#[from] crate::serde::urlencoded::ser::Error), /// Header serialization failed. #[error("header serialization failed: {0}")] Header(#[from] http::header::InvalidHeaderValue), /// HTTP request construction failed. #[error("HTTP request construction failed: {0}")] Http(#[from] http::Error), } /// An error when converting a http request to one of ruma's endpoint-specific request types. #[derive(Debug, Error)] #[non_exhaustive] pub enum FromHttpRequestError { /// Deserialization failed #[error("deserialization failed: {0}")] Deserialization(DeserializationError), /// HTTP method mismatch #[error("http method mismatch: expected {expected}, received: {received}")] MethodMismatch { /// expected http method expected: http::method::Method, /// received http method received: http::method::Method, }, } impl From for FromHttpRequestError where T: Into, { fn from(err: T) -> Self { Self::Deserialization(err.into()) } } /// An error when converting a http response to one of Ruma's endpoint-specific response types. #[derive(Debug)] #[non_exhaustive] pub enum FromHttpResponseError { /// Deserialization failed Deserialization(DeserializationError), /// The server returned a non-success status Server(ServerError), } impl FromHttpResponseError { /// Map `FromHttpResponseError` to `FromHttpResponseError` by applying a function to a /// contained `Server` value, leaving a `Deserialization` value untouched. pub fn map( self, f: impl FnOnce(ServerError) -> ServerError, ) -> FromHttpResponseError { match self { Self::Deserialization(d) => FromHttpResponseError::Deserialization(d), Self::Server(s) => FromHttpResponseError::Server(f(s)), } } } impl FromHttpResponseError> { /// Transpose `FromHttpResponseError>` to `Result, F>`. pub fn transpose(self) -> Result, F> { match self { Self::Deserialization(d) => Ok(FromHttpResponseError::Deserialization(d)), Self::Server(s) => s.transpose().map(FromHttpResponseError::Server), } } } impl fmt::Display for FromHttpResponseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Deserialization(err) => write!(f, "deserialization failed: {err}"), Self::Server(err) => write!(f, "the server returned an error: {err}"), } } } impl From> for FromHttpResponseError { fn from(err: ServerError) -> Self { Self::Server(err) } } impl From for FromHttpResponseError where T: Into, { fn from(err: T) -> Self { Self::Deserialization(err.into()) } } impl StdError for FromHttpResponseError {} /// An error was reported by the server (HTTP status code 4xx or 5xx) #[derive(Debug)] #[allow(clippy::exhaustive_enums)] pub enum ServerError { /// An error that is expected to happen under certain circumstances and /// that has a well-defined structure Known(E), /// An error of unexpected type of structure Unknown(DeserializationError), } impl ServerError { /// Map `ServerError` to `ServerError` by applying a function to a contained `Known` /// value, leaving an `Unknown` value untouched. pub fn map(self, f: impl FnOnce(E) -> F) -> ServerError { match self { Self::Known(k) => ServerError::Known(f(k)), Self::Unknown(u) => ServerError::Unknown(u), } } } impl ServerError> { /// Transpose `ServerError>` to `Result, F>`. pub fn transpose(self) -> Result, F> { match self { Self::Known(Ok(k)) => Ok(ServerError::Known(k)), Self::Known(Err(e)) => Err(e), Self::Unknown(u) => Ok(ServerError::Unknown(u)), } } } impl fmt::Display for ServerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ServerError::Known(e) => fmt::Display::fmt(e, f), ServerError::Unknown(res_err) => fmt::Display::fmt(res_err, f), } } } impl StdError for ServerError {} /// An error when converting a http request / response to one of ruma's endpoint-specific request / /// response types. #[derive(Debug, Error)] #[non_exhaustive] pub enum DeserializationError { /// Encountered invalid UTF-8. #[error(transparent)] Utf8(#[from] std::str::Utf8Error), /// JSON deserialization failed. #[error(transparent)] Json(#[from] serde_json::Error), /// Query parameter deserialization failed. #[error(transparent)] Query(#[from] crate::serde::urlencoded::de::Error), /// Got an invalid identifier. #[error(transparent)] Ident(#[from] crate::IdParseError), /// Header value deserialization failed. #[error(transparent)] Header(#[from] HeaderDeserializationError), } impl From for DeserializationError { fn from(err: std::convert::Infallible) -> Self { match err {} } } impl From for DeserializationError { fn from(err: http::header::ToStrError) -> Self { Self::Header(HeaderDeserializationError::ToStrError(err)) } } /// An error with the http headers. #[derive(Debug, Error)] #[non_exhaustive] pub enum HeaderDeserializationError { /// Failed to convert `http::header::HeaderValue` to `str`. #[error("{0}")] ToStrError(http::header::ToStrError), /// The given required header is missing. #[error("missing header `{0}`")] MissingHeader(String), } /// An error that happens when Ruma cannot understand a Matrix version. #[derive(Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct UnknownVersionError; impl fmt::Display for UnknownVersionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "version string was unknown") } } impl StdError for UnknownVersionError {} ruma-common-0.10.5/src/api/metadata.rs000064400000000000000000000224431046102023000156340ustar 00000000000000use std::{ fmt::{self, Display}, str::FromStr, }; use http::Method; use super::{error::UnknownVersionError, AuthScheme}; use crate::RoomVersionId; /// Metadata about an API endpoint. #[derive(Clone, Debug)] #[allow(clippy::exhaustive_structs)] pub struct Metadata { /// A human-readable description of the endpoint. pub description: &'static str, /// The HTTP method used by this endpoint. pub method: Method, /// A unique identifier for this endpoint. pub name: &'static str, /// The unstable path of this endpoint's URL, often `None`, used for developmental /// purposes. pub unstable_path: Option<&'static str>, /// The pre-v1.1 version of this endpoint's URL, `None` for post-v1.1 endpoints, /// supplemental to `stable_path`. pub r0_path: Option<&'static str>, /// The path of this endpoint's URL, with variable names where path parameters should be /// filled in during a request. pub stable_path: Option<&'static str>, /// Whether or not this endpoint is rate limited by the server. pub rate_limited: bool, /// What authentication scheme the server uses for this endpoint. pub authentication: AuthScheme, /// The matrix version that this endpoint was added in. /// /// Is None when this endpoint is unstable/unreleased. pub added: Option, /// The matrix version that deprecated this endpoint. /// /// Deprecation often precedes one matrix version before removal. /// /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request) /// emit a warning, see the corresponding documentation for more information. pub deprecated: Option, /// The matrix version that removed this endpoint. /// /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request) /// emit an error, see the corresponding documentation for more information. pub removed: Option, } impl Metadata { /// Will decide how a particular set of matrix versions sees an endpoint. /// /// It will pick `Stable` over `R0` and `Unstable`. It'll return `Deprecated` or `Removed` only /// if all versions denote it. /// /// In other words, if in any version it tells it supports the endpoint in a stable fashion, /// this will return `Stable`, even if some versions in this set will denote deprecation or /// removal. /// /// If resulting [`VersioningDecision`] is `Stable`, it will also detail if any version denoted /// deprecation or removal. pub fn versioning_decision_for(&self, versions: &[MatrixVersion]) -> VersioningDecision { let greater_or_equal_any = |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version)); let greater_or_equal_all = |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version)); // Check if all versions removed this endpoint. if self.removed.map(greater_or_equal_all).unwrap_or(false) { return VersioningDecision::Removed; } // Check if *any* version marks this endpoint as stable. if self.added.map(greater_or_equal_any).unwrap_or(false) { let all_deprecated = self.deprecated.map(greater_or_equal_all).unwrap_or(false); return VersioningDecision::Stable { any_deprecated: all_deprecated || self.deprecated.map(greater_or_equal_any).unwrap_or(false), all_deprecated, any_removed: self.removed.map(greater_or_equal_any).unwrap_or(false), }; } VersioningDecision::Unstable } } /// A versioning "decision" derived from a set of matrix versions. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[allow(clippy::exhaustive_enums)] pub enum VersioningDecision { /// The unstable endpoint should be used. Unstable, /// The stable endpoint should be used. /// /// Note, in the special case that all versions note [v1.0](MatrixVersion::V1_0), and the /// [`r0_path`](Metadata::r0_path) is not `None`, that path should be used. Stable { /// If any version denoted deprecation. any_deprecated: bool, /// If *all* versions denoted deprecation. all_deprecated: bool, /// If any version denoted removal. any_removed: bool, }, /// This endpoint was removed in all versions, it should not be used. Removed, } /// The Matrix versions Ruma currently understands to exist. /// /// Matrix, since fall 2021, has a quarterly release schedule, using a global `vX.Y` versioning /// scheme. /// /// Every new minor version denotes stable support for endpoints in a *relatively* /// backwards-compatible manner. /// /// Matrix has a deprecation policy, read more about it here: . /// /// Ruma keeps track of when endpoints are added, deprecated, and removed. It'll automatically /// select the right endpoint stability variation to use depending on which Matrix versions you /// pass to [`try_into_http_request`](super::OutgoingRequest::try_into_http_request), see its /// respective documentation for more information. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum MatrixVersion { /// Version 1.0 of the Matrix specification. /// /// Retroactively defined as . V1_0, /// Version 1.1 of the Matrix specification, released in Q4 2021. /// /// See . V1_1, /// Version 1.2 of the Matrix specification, released in Q1 2022. /// /// See . V1_2, /// Version 1.3 of the Matrix specification, released in Q2 2022. /// /// See . V1_3, } impl TryFrom<&str> for MatrixVersion { type Error = UnknownVersionError; fn try_from(value: &str) -> Result { use MatrixVersion::*; Ok(match value { // FIXME: these are likely not entirely correct; https://github.com/ruma/ruma/issues/852 "v1.0" | // Additional definitions according to https://spec.matrix.org/v1.2/#legacy-versioning "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0, "v1.1" => V1_1, "v1.2" => V1_2, "v1.3" => V1_3, _ => return Err(UnknownVersionError), }) } } impl FromStr for MatrixVersion { type Err = UnknownVersionError; fn from_str(s: &str) -> Result { Self::try_from(s) } } impl MatrixVersion { /// Checks whether a version is compatible with another. /// /// A is compatible with B as long as B is equal or less, so long as A and B have the same /// major versions. /// /// For example, v1.2 is compatible with v1.1, as it is likely only some additions of /// endpoints on top of v1.1, but v1.1 would not be compatible with v1.2, as v1.1 /// cannot represent all of v1.2, in a manner similar to set theory. /// /// Warning: Matrix has a deprecation policy, and Matrix versioning is not as /// straight-forward as this function makes it out to be. This function only exists /// to prune major version differences, and versions too new for `self`. /// /// This (considering if major versions are the same) is equivalent to a `self >= other` /// check. pub fn is_superset_of(self, other: Self) -> bool { let (major_l, minor_l) = self.into_parts(); let (major_r, minor_r) = other.into_parts(); major_l == major_r && minor_l >= minor_r } /// Decompose the Matrix version into its major and minor number. pub fn into_parts(self) -> (u8, u8) { match self { MatrixVersion::V1_0 => (1, 0), MatrixVersion::V1_1 => (1, 1), MatrixVersion::V1_2 => (1, 2), MatrixVersion::V1_3 => (1, 3), } } /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`. pub fn from_parts(major: u8, minor: u8) -> Result { match (major, minor) { (1, 0) => Ok(MatrixVersion::V1_0), (1, 1) => Ok(MatrixVersion::V1_1), (1, 2) => Ok(MatrixVersion::V1_2), (1, 3) => Ok(MatrixVersion::V1_3), _ => Err(UnknownVersionError), } } /// Get the default [`RoomVersionId`] for this `MatrixVersion`. pub fn default_room_version(&self) -> RoomVersionId { match self { // MatrixVersion::V1_0 // | MatrixVersion::V1_1 // | MatrixVersion::V1_2 => RoomVersionId::V6, // MatrixVersion::V1_3 => RoomVersionId::V9, } } } impl Display for MatrixVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (major, minor) = self.into_parts(); f.write_str(&format!("v{major}.{minor}")) } } ruma-common-0.10.5/src/api.rs000064400000000000000000000432351046102023000140560ustar 00000000000000//! Core types used to define the requests and responses for each endpoint in the various //! [Matrix API specifications][apis]. //! //! When implementing a new Matrix API, each endpoint has a request type which implements //! [`IncomingRequest`] and [`OutgoingRequest`], and a response type connected via an associated //! type. //! //! An implementation of [`IncomingRequest`] or [`OutgoingRequest`] contains all the information //! about the HTTP method, the path and input parameters for requests, and the structure of a //! successful response. Such types can then be used by client code to make requests, and by server //! code to fulfill those requests. //! //! [apis]: https://spec.matrix.org/v1.2/#matrix-apis use std::{convert::TryInto as _, error::Error as StdError, fmt}; use bytes::BufMut; use tracing::warn; use crate::UserId; /// Generates [`IncomingRequest`] and [`OutgoingRequest`] from a concise definition. /// /// The macro expects the following structure as input: /// /// ```text /// ruma_api! { /// metadata: { /// description: &'static str, /// method: http::Method, /// name: &'static str, /// path: &'static str, /// rate_limited: bool, /// authentication: ruma_common::api::AuthScheme, /// } /// /// request: { /// // Struct fields for each piece of data required /// // to make a request to this API endpoint. /// } /// /// response: { /// // Struct fields for each piece of data expected /// // in the response from this API endpoint. /// } /// /// // The error returned when a response fails, defaults to `MatrixError`. /// error: path::to::Error /// } /// ``` /// /// This will generate a [`Metadata`] value to be used for the associated constants of /// [`IncomingRequest`] and [`OutgoingRequest`], single `Request` and `Response` structs, and /// the necessary trait implementations to convert the request into a `http::Request` and to /// create a response from a `http::Response` and vice versa. /// /// The details of each of the three sections of the macros are documented below. /// /// ## Metadata /// /// * `description`: A short description of what the endpoint does. /// * `method`: The HTTP method used for requests to the endpoint. It's not necessary to import /// `http::Method`'s associated constants. Just write the value as if it was imported, e.g. /// `GET`. /// * `name`: A unique name for the endpoint. Generally this will be the same as the containing /// module. /// * `path`: The path component of the URL for the endpoint, e.g. "/foo/bar". Components of /// the path that are parameterized can indicate a variable by using a Rust identifier /// prefixed with a colon, e.g. `/foo/:some_parameter`. A corresponding query string /// parameter will be expected in the request struct (see below for details). /// * `rate_limited`: Whether or not the endpoint enforces rate limiting on requests. /// * `authentication`: What authentication scheme the endpoint uses. /// /// ## Request /// /// The request block contains normal struct field definitions. Doc comments and attributes are /// allowed as normal. There are also a few special attributes available to control how the /// struct is converted into an `http::Request`: /// /// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP /// headers on the request. The value must implement `AsRef`. Generally this is a /// `String`. The attribute value shown above as `HEADER_NAME` must be a header name constant /// from `http::header`, e.g. `CONTENT_TYPE`. /// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path /// component of the request URL. /// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query /// string. /// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any /// type that implements `IntoIterator` (e.g. `HashMap`, can be used for cases where an endpoint supports arbitrary query parameters. /// /// Any field that does not include one of these attributes will be part of the request's JSON /// body. /// /// ## Response /// /// Like the request block, the response block consists of normal struct field definitions. /// Doc comments and attributes are allowed as normal. /// There is also a special attribute available to control how the struct is created from a /// `http::Request`: /// /// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP /// headers on the response. The value must implement `AsRef`. Generally this is a /// `String`. The attribute value shown above as `HEADER_NAME` must be a header name constant /// from `http::header`, e.g. `CONTENT_TYPE`. /// /// Any field that does not include the above attribute will be expected in the response's JSON /// body. /// /// ## Newtype bodies /// /// Both the request and response block also support "newtype bodies" by using the /// `#[ruma_api(body)]` attribute on a field. If present on a field, the entire request or /// response body will be treated as the value of the field. This allows you to treat the /// entire request or response body as a specific type, rather than a JSON object with named /// fields. Only one field in each struct can be marked with this attribute. It is an error to /// have a newtype body field and normal body fields within the same struct. /// /// There is another kind of newtype body that is enabled with `#[ruma_api(raw_body)]`. It is /// used for endpoints in which the request or response body can be arbitrary bytes instead of /// a JSON objects. A field with `#[ruma_api(raw_body)]` needs to have the type `Vec`. /// /// # Examples /// /// ``` /// pub mod some_endpoint { /// use ruma_common::api::ruma_api; /// /// ruma_api! { /// metadata: { /// description: "Does something.", /// method: POST, /// name: "some_endpoint", /// stable_path: "/_matrix/some/endpoint/:baz", /// rate_limited: false, /// authentication: None, /// added: 1.1, /// } /// /// request: { /// pub foo: String, /// /// #[ruma_api(header = CONTENT_TYPE)] /// pub content_type: String, /// /// #[ruma_api(query)] /// pub bar: String, /// /// #[ruma_api(path)] /// pub baz: String, /// } /// /// response: { /// #[ruma_api(header = CONTENT_TYPE)] /// pub content_type: String, /// /// pub value: String, /// } /// } /// } /// /// pub mod newtype_body_endpoint { /// use ruma_common::api::ruma_api; /// use serde::{Deserialize, Serialize}; /// /// #[derive(Clone, Debug, Deserialize, Serialize)] /// pub struct MyCustomType { /// pub foo: String, /// } /// /// ruma_api! { /// metadata: { /// description: "Does something.", /// method: PUT, /// name: "newtype_body_endpoint", /// stable_path: "/_matrix/some/newtype/body/endpoint", /// rate_limited: false, /// authentication: None, /// added: 1.1, /// } /// /// request: { /// #[ruma_api(raw_body)] /// pub file: &'a [u8], /// } /// /// response: { /// #[ruma_api(body)] /// pub my_custom_type: MyCustomType, /// } /// } /// } /// ``` pub use ruma_macros::ruma_api; pub mod error; mod metadata; pub use metadata::{MatrixVersion, Metadata, VersioningDecision}; use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError}; /// An enum to control whether an access token should be added to outgoing requests #[derive(Clone, Copy, Debug)] #[allow(clippy::exhaustive_enums)] pub enum SendAccessToken<'a> { /// Add the given access token to the request only if the `METADATA` on the request requires /// it. IfRequired(&'a str), /// Always add the access token. Always(&'a str), /// Don't add an access token. /// /// This will lead to an error if the request endpoint requires authentication None, } impl<'a> SendAccessToken<'a> { /// Get the access token for an endpoint that requires one. /// /// Returns `Some(_)` if `self` contains an access token. pub fn get_required_for_endpoint(self) -> Option<&'a str> { match self { Self::IfRequired(tok) | Self::Always(tok) => Some(tok), Self::None => None, } } /// Get the access token for an endpoint that should not require one. /// /// Returns `Some(_)` only if `self` is `SendAccessToken::Always(_)`. pub fn get_not_required_for_endpoint(self) -> Option<&'a str> { match self { Self::Always(tok) => Some(tok), Self::IfRequired(_) | Self::None => None, } } } /// A request type for a Matrix API endpoint, used for sending requests. pub trait OutgoingRequest: Sized + Clone { /// A type capturing the expected error conditions the server can return. type EndpointError: EndpointError; /// Response type returned when the request is successful. type IncomingResponse: IncomingResponse; /// Metadata about the endpoint. const METADATA: Metadata; /// Tries to convert this request into an `http::Request`. /// /// On endpoints with authentication, when adequate information isn't provided through /// access_token, this could result in an error. It may also fail with a serialization error /// in case of bugs in Ruma though. /// /// It may also fail if, for every version in `considering_versions`; /// - The endpoint is too old, and has been removed in all versions. /// ([`EndpointRemoved`](error::IntoHttpError::EndpointRemoved)) /// - The endpoint is too new, and no unstable path is known for this endpoint. /// ([`NoUnstablePath`](error::IntoHttpError::NoUnstablePath)) /// /// Finally, this will emit a warning through `tracing` if it detects if any version in /// `considering_versions` has deprecated this endpoint. /// /// The endpoints path will be appended to the given `base_url`, for example /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the /// `base_url` to have a trailing slash. If it has one however, it will be ignored. fn try_into_http_request( self, base_url: &str, access_token: SendAccessToken<'_>, considering_versions: &'_ [MatrixVersion], ) -> Result, IntoHttpError>; } /// A response type for a Matrix API endpoint, used for receiving responses. pub trait IncomingResponse: Sized { /// A type capturing the expected error conditions the server can return. type EndpointError: EndpointError; /// Tries to convert the given `http::Response` into this response type. fn try_from_http_response>( response: http::Response, ) -> Result>; } /// An extension to [`OutgoingRequest`] which provides Appservice specific methods. pub trait OutgoingRequestAppserviceExt: OutgoingRequest { /// Tries to convert this request into an `http::Request` and appends a virtual `user_id` to /// [assert Appservice identity][id_assert]. /// /// [id_assert]: https://spec.matrix.org/v1.2/application-service-api/#identity-assertion fn try_into_http_request_with_user_id( self, base_url: &str, access_token: SendAccessToken<'_>, user_id: &UserId, considering_versions: &'_ [MatrixVersion], ) -> Result, IntoHttpError> { let mut http_request = self.try_into_http_request(base_url, access_token, considering_versions)?; let user_id_query = crate::serde::urlencoded::to_string([("user_id", user_id)])?; let uri = http_request.uri().to_owned(); let mut parts = uri.into_parts(); let path_and_query_with_user_id = match &parts.path_and_query { Some(path_and_query) => match path_and_query.query() { Some(_) => format!("{path_and_query}&{user_id_query}"), None => format!("{path_and_query}?{user_id_query}"), }, None => format!("/?{user_id_query}"), }; parts.path_and_query = Some(path_and_query_with_user_id.try_into().map_err(http::Error::from)?); *http_request.uri_mut() = parts.try_into().map_err(http::Error::from)?; Ok(http_request) } } impl OutgoingRequestAppserviceExt for T {} /// A request type for a Matrix API endpoint, used for receiving requests. pub trait IncomingRequest: Sized { /// A type capturing the error conditions that can be returned in the response. type EndpointError: EndpointError; /// Response type to return when the request is successful. type OutgoingResponse: OutgoingResponse; /// Metadata about the endpoint. const METADATA: Metadata; /// Tries to turn the given `http::Request` into this request type, /// together with the corresponding path arguments. /// /// Note: The strings in path_args need to be percent-decoded. fn try_from_http_request( req: http::Request, path_args: &[S], ) -> Result where B: AsRef<[u8]>, S: AsRef; } /// A request type for a Matrix API endpoint, used for sending responses. pub trait OutgoingResponse { /// Tries to convert this response into an `http::Response`. /// /// This method should only fail when when invalid header values are specified. It may also /// fail with a serialization error in case of bugs in Ruma though. fn try_into_http_response( self, ) -> Result, IntoHttpError>; } /// Gives users the ability to define their own serializable / deserializable errors. pub trait EndpointError: OutgoingResponse + StdError + Sized + Send + 'static { /// Tries to construct `Self` from an `http::Response`. /// /// This will always return `Err` variant when no `error` field is defined in /// the `ruma_api` macro. fn try_from_http_response>( response: http::Response, ) -> Result; } /// Marker trait for requests that don't require authentication, for the client side. pub trait OutgoingNonAuthRequest: OutgoingRequest {} /// Marker trait for requests that don't require authentication, for the server side. pub trait IncomingNonAuthRequest: IncomingRequest {} /// Authentication scheme used by the endpoint. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum AuthScheme { /// No authentication is performed. None, /// Authentication is performed by including an access token in the `Authentication` http /// header, or an `access_token` query parameter. /// /// It is recommended to use the header over the query parameter. AccessToken, /// Authentication is performed by including X-Matrix signatures in the request headers, /// as defined in the federation API. ServerSignatures, /// Authentication is performed by setting the `access_token` query parameter. QueryOnlyAccessToken, } // This function helps picks the right path (or an error) from a set of matrix versions. // // This function needs to be public, yet hidden, as all `try_into_http_request`s would be using it. #[doc(hidden)] pub fn select_path<'a>( versions: &'_ [MatrixVersion], metadata: &'_ Metadata, unstable: Option>, r0: Option>, stable: Option>, ) -> Result, IntoHttpError> { match metadata.versioning_decision_for(versions) { VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved( metadata.removed.expect("VersioningDecision::Removed implies metadata.removed"), )), VersioningDecision::Stable { any_deprecated, all_deprecated, any_removed } => { if any_removed { if all_deprecated { warn!( "endpoint {} is removed in some (and deprecated in ALL) of the following versions: {:?}", metadata.name, versions ); } else if any_deprecated { warn!( "endpoint {} is removed (and deprecated) in some of the following versions: {:?}", metadata.name, versions ); } else { unreachable!("any_removed implies *_deprecated"); } } else if all_deprecated { warn!( "endpoint {} is deprecated in ALL of the following versions: {:?}", metadata.name, versions ); } else if any_deprecated { warn!( "endpoint {} is deprecated in some of the following versions: {:?}", metadata.name, versions ); } if let Some(r0) = r0 { if versions.iter().all(|&v| v == MatrixVersion::V1_0) { // Endpoint was added in 1.0, we return the r0 variant. return Ok(r0); } } Ok(stable.expect("metadata.added enforces the stable path to exist")) } VersioningDecision::Unstable => unstable.ok_or(IntoHttpError::NoUnstablePath), } } ruma-common-0.10.5/src/authentication.rs000064400000000000000000000005641046102023000163220ustar 00000000000000//! Common types for authentication. use crate::{serde::StringEnum, PrivOwnedStr}; /// Access token types. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum TokenType { /// Bearer token type Bearer, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/canonical_json/value.rs000064400000000000000000000241351046102023000173770ustar 00000000000000use std::{collections::BTreeMap, fmt}; use js_int::Int; use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; use serde_json::{to_string as to_json_string, Value as JsonValue}; use super::CanonicalJsonError; /// The inner type of `CanonicalJsonValue::Object`. #[cfg(feature = "canonical-json")] pub type CanonicalJsonObject = BTreeMap; /// Represents a canonical JSON value as per the Matrix specification. #[cfg(feature = "canonical-json")] #[derive(Clone, Eq, PartialEq)] #[allow(clippy::exhaustive_enums)] pub enum CanonicalJsonValue { /// Represents a JSON null value. /// /// ``` /// # use serde_json::json; /// # use ruma_common::CanonicalJsonValue; /// let v: CanonicalJsonValue = json!(null).try_into().unwrap(); /// ``` Null, /// Represents a JSON boolean. /// /// ``` /// # use serde_json::json; /// # use ruma_common::CanonicalJsonValue; /// let v: CanonicalJsonValue = json!(true).try_into().unwrap(); /// ``` Bool(bool), /// Represents a JSON integer. /// /// ``` /// # use serde_json::json; /// # use ruma_common::CanonicalJsonValue; /// let v: CanonicalJsonValue = json!(12).try_into().unwrap(); /// ``` Integer(Int), /// Represents a JSON string. /// /// ``` /// # use serde_json::json; /// # use ruma_common::CanonicalJsonValue; /// let v: CanonicalJsonValue = json!("a string").try_into().unwrap(); /// ``` String(String), /// Represents a JSON array. /// /// ``` /// # use serde_json::json; /// # use ruma_common::CanonicalJsonValue; /// let v: CanonicalJsonValue = json!(["an", "array"]).try_into().unwrap(); /// ``` Array(Vec), /// Represents a JSON object. /// /// The map is backed by a BTreeMap to guarantee the sorting of keys. /// /// ``` /// # use serde_json::json; /// # use ruma_common::CanonicalJsonValue; /// let v: CanonicalJsonValue = json!({ "an": "object" }).try_into().unwrap(); /// ``` Object(CanonicalJsonObject), } impl CanonicalJsonValue { /// If the `CanonicalJsonValue` is a `Bool`, return the inner value. pub fn as_bool(&self) -> Option { match self { Self::Bool(b) => Some(*b), _ => None, } } /// If the `CanonicalJsonValue` is an `Integer`, return the inner value. pub fn as_integer(&self) -> Option { match self { Self::Integer(i) => Some(*i), _ => None, } } /// If the `CanonicalJsonValue` is a `String`, return a reference to the inner value. pub fn as_str(&self) -> Option<&str> { match self { Self::String(s) => Some(s), _ => None, } } /// If the `CanonicalJsonValue` is an `Array`, return a reference to the inner value. pub fn as_array(&self) -> Option<&[CanonicalJsonValue]> { match self { Self::Array(a) => Some(a), _ => None, } } /// If the `CanonicalJsonValue` is an `Object`, return a reference to the inner value. pub fn as_object(&self) -> Option<&CanonicalJsonObject> { match self { Self::Object(o) => Some(o), _ => None, } } /// If the `CanonicalJsonValue` is an `Array`, return a mutable reference to the inner value. pub fn as_array_mut(&mut self) -> Option<&mut Vec> { match self { Self::Array(a) => Some(a), _ => None, } } /// If the `CanonicalJsonValue` is an `Object`, return a mutable reference to the inner value. pub fn as_object_mut(&mut self) -> Option<&mut CanonicalJsonObject> { match self { Self::Object(o) => Some(o), _ => None, } } /// Returns `true` if the `CanonicalJsonValue` is a `Bool`. pub fn is_bool(&self) -> bool { matches!(self, Self::Bool(_)) } /// Returns `true` if the `CanonicalJsonValue` is an `Integer`. pub fn is_integer(&self) -> bool { matches!(self, Self::Integer(_)) } /// Returns `true` if the `CanonicalJsonValue` is a `String`. pub fn is_string(&self) -> bool { matches!(self, Self::String(_)) } /// Returns `true` if the `CanonicalJsonValue` is an `Array`. pub fn is_array(&self) -> bool { matches!(self, Self::Array(_)) } /// Returns `true` if the `CanonicalJsonValue` is an `Object`. pub fn is_object(&self) -> bool { matches!(self, Self::Object(_)) } } impl Default for CanonicalJsonValue { fn default() -> Self { Self::Null } } impl fmt::Debug for CanonicalJsonValue { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Self::Null => formatter.debug_tuple("Null").finish(), Self::Bool(v) => formatter.debug_tuple("Bool").field(&v).finish(), Self::Integer(ref v) => fmt::Debug::fmt(v, formatter), Self::String(ref v) => formatter.debug_tuple("String").field(v).finish(), Self::Array(ref v) => { formatter.write_str("Array(")?; fmt::Debug::fmt(v, formatter)?; formatter.write_str(")") } Self::Object(ref v) => { formatter.write_str("Object(")?; fmt::Debug::fmt(v, formatter)?; formatter.write_str(")") } } } } impl fmt::Display for CanonicalJsonValue { /// Display this value as a string. /// /// This `Display` implementation is intentionally unaffected by any formatting parameters, /// because adding extra whitespace or otherwise pretty-printing it would make it not the /// canonical form anymore. /// /// If you want to pretty-print a `CanonicalJsonValue` for debugging purposes, use /// one of `serde_json::{to_string_pretty, to_vec_pretty, to_writer_pretty}`. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", to_json_string(&self).map_err(|_| fmt::Error)?) } } impl TryFrom for CanonicalJsonValue { type Error = CanonicalJsonError; fn try_from(val: JsonValue) -> Result { Ok(match val { JsonValue::Bool(b) => Self::Bool(b), JsonValue::Number(num) => Self::Integer( Int::try_from(num.as_i64().ok_or(CanonicalJsonError::IntConvert)?) .map_err(|_| CanonicalJsonError::IntConvert)?, ), JsonValue::Array(vec) => { Self::Array(vec.into_iter().map(TryInto::try_into).collect::, _>>()?) } JsonValue::String(string) => Self::String(string), JsonValue::Object(obj) => Self::Object( obj.into_iter() .map(|(k, v)| Ok((k, v.try_into()?))) .collect::>()?, ), JsonValue::Null => Self::Null, }) } } impl From for JsonValue { fn from(val: CanonicalJsonValue) -> Self { match val { CanonicalJsonValue::Bool(b) => Self::Bool(b), CanonicalJsonValue::Integer(int) => Self::Number(i64::from(int).into()), CanonicalJsonValue::String(string) => Self::String(string), CanonicalJsonValue::Array(vec) => { Self::Array(vec.into_iter().map(Into::into).collect()) } CanonicalJsonValue::Object(obj) => { Self::Object(obj.into_iter().map(|(k, v)| (k, v.into())).collect()) } CanonicalJsonValue::Null => Self::Null, } } } macro_rules! variant_impls { ($variant:ident($ty:ty)) => { impl From<$ty> for CanonicalJsonValue { fn from(val: $ty) -> Self { Self::$variant(val) } } impl PartialEq<$ty> for CanonicalJsonValue { fn eq(&self, other: &$ty) -> bool { match self { Self::$variant(val) => val == other, _ => false, } } } impl PartialEq for $ty { fn eq(&self, other: &CanonicalJsonValue) -> bool { match other { CanonicalJsonValue::$variant(val) => self == val, _ => false, } } } }; } variant_impls!(Bool(bool)); variant_impls!(Integer(Int)); variant_impls!(String(String)); variant_impls!(Array(Vec)); variant_impls!(Object(CanonicalJsonObject)); impl Serialize for CanonicalJsonValue { #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Self::Null => serializer.serialize_unit(), Self::Bool(b) => serializer.serialize_bool(*b), Self::Integer(n) => n.serialize(serializer), Self::String(s) => serializer.serialize_str(s), Self::Array(v) => v.serialize(serializer), Self::Object(m) => { use serde::ser::SerializeMap; let mut map = serializer.serialize_map(Some(m.len()))?; for (k, v) in m { map.serialize_entry(k, v)?; } map.end() } } } } impl<'de> Deserialize<'de> for CanonicalJsonValue { #[inline] fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let val = JsonValue::deserialize(deserializer)?; val.try_into().map_err(serde::de::Error::custom) } } #[cfg(test)] mod tests { use serde_json::json; use super::CanonicalJsonValue; #[test] fn to_string() { const CANONICAL_STR: &str = r#"{"city":"London","street":"10 Downing Street"}"#; let json: CanonicalJsonValue = json!({ "city": "London", "street": "10 Downing Street" }).try_into().unwrap(); assert_eq!(format!("{json}"), CANONICAL_STR); assert_eq!(format!("{:#}", json), CANONICAL_STR); } } ruma-common-0.10.5/src/canonical_json.rs000064400000000000000000000260701046102023000162630ustar 00000000000000//! Canonical JSON types and related functions. use std::{fmt, mem}; use serde::Serialize; use serde_json::Value as JsonValue; mod value; use crate::RoomVersionId; pub use self::value::{CanonicalJsonObject, CanonicalJsonValue}; /// The set of possible errors when serializing to canonical JSON. #[cfg(feature = "canonical-json")] #[derive(Debug)] #[allow(clippy::exhaustive_enums)] pub enum CanonicalJsonError { /// The numeric value failed conversion to js_int::Int. IntConvert, /// An error occurred while serializing/deserializing. SerDe(serde_json::Error), } impl fmt::Display for CanonicalJsonError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CanonicalJsonError::IntConvert => { f.write_str("number found is not a valid `js_int::Int`") } CanonicalJsonError::SerDe(err) => write!(f, "serde Error: {err}"), } } } impl std::error::Error for CanonicalJsonError {} /// Errors that can happen in redaction. #[cfg(feature = "canonical-json")] #[derive(Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum RedactionError { /// The field `field` is not of the correct type `of_type` ([`JsonType`]). #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] NotOfType { /// The field name. field: String, /// The expected JSON type. of_type: JsonType, }, /// The given required field is missing from a JSON object. JsonFieldMissingFromObject(String), } impl fmt::Display for RedactionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RedactionError::NotOfType { field, of_type } => { write!(f, "Value in {field:?} must be a JSON {of_type:?}") } RedactionError::JsonFieldMissingFromObject(field) => { write!(f, "JSON object must contain the field {field:?}") } } } } impl std::error::Error for RedactionError {} impl RedactionError { fn not_of_type(target: &str, of_type: JsonType) -> Self { Self::NotOfType { field: target.to_owned(), of_type } } fn field_missing_from_object(target: &str) -> Self { Self::JsonFieldMissingFromObject(target.to_owned()) } } /// A JSON type enum for [`RedactionError`] variants. #[derive(Debug)] #[allow(clippy::exhaustive_enums)] pub enum JsonType { /// A JSON Object. Object, /// A JSON String. String, /// A JSON Integer. Integer, /// A JSON Array. Array, /// A JSON Boolean. Boolean, /// JSON Null. Null, } /// Fallible conversion from a `serde_json::Map` to a `CanonicalJsonObject`. pub fn try_from_json_map( json: serde_json::Map, ) -> Result { json.into_iter().map(|(k, v)| Ok((k, v.try_into()?))).collect() } /// Fallible conversion from any value that impl's `Serialize` to a `CanonicalJsonValue`. pub fn to_canonical_value( value: T, ) -> Result { serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)?.try_into() } /// Redacts an event using the rules specified in the Matrix client-server specification. /// /// This is part of the process of signing an event. /// /// Redaction is also suggested when verifying an event with `verify_event` returns /// `Verified::Signatures`. See the documentation for `Verified` for details. /// /// Returns a new JSON object with all applicable fields redacted. /// /// # Parameters /// /// * object: A JSON object to redact. /// /// # Errors /// /// Returns an error if: /// /// * `object` contains a field called `content` that is not a JSON object. /// * `object` contains a field called `hashes` that is not a JSON object. /// * `object` contains a field called `signatures` that is not a JSON object. /// * `object` is missing the `type` field or the field is not a JSON string. pub fn redact( object: &CanonicalJsonObject, version: &RoomVersionId, ) -> Result { let mut val = object.clone(); redact_in_place(&mut val, version)?; Ok(val) } /// Redacts an event using the rules specified in the Matrix client-server specification. /// /// Functionally equivalent to `redact`, only; /// * upon error, the event is not touched. /// * this'll redact the event in-place. pub fn redact_in_place( event: &mut CanonicalJsonObject, version: &RoomVersionId, ) -> Result<(), RedactionError> { // Get the content keys here even if they're only needed inside the branch below, because we // can't teach rust that this is a disjoint borrow with `get_mut("content")`. let allowed_content_keys: &[&str] = match event.get("type") { Some(CanonicalJsonValue::String(event_type)) => { allowed_content_keys_for(event_type, version) } Some(_) => return Err(RedactionError::not_of_type("type", JsonType::String)), None => return Err(RedactionError::field_missing_from_object("type")), }; if let Some(content_value) = event.get_mut("content") { let content = match content_value { CanonicalJsonValue::Object(map) => map, _ => return Err(RedactionError::not_of_type("content", JsonType::Object)), }; object_retain_keys(content, allowed_content_keys); } let mut old_event = mem::take(event); for &key in ALLOWED_KEYS { if let Some(value) = old_event.remove(key) { event.insert(key.to_owned(), value); } } Ok(()) } /// Redacts event content using the rules specified in the Matrix client-server specification. /// /// Edits the `object` in-place. pub fn redact_content_in_place( object: &mut CanonicalJsonObject, version: &RoomVersionId, event_type: impl AsRef, ) { object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version)); } fn object_retain_keys(object: &mut CanonicalJsonObject, keys: &[&str]) { let mut old_content = mem::take(object); for &key in keys { if let Some(value) = old_content.remove(key) { object.insert(key.to_owned(), value); } } } /// The fields that are allowed to remain in an event during redaction. static ALLOWED_KEYS: &[&str] = &[ "event_id", "type", "room_id", "sender", "state_key", "content", "hashes", "signatures", "depth", "prev_events", "prev_state", "auth_events", "origin", "origin_server_ts", "membership", ]; fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'static [&'static str] { match event_type { "m.room.member" => match version { RoomVersionId::V9 | RoomVersionId::V10 => { &["membership", "join_authorised_via_users_server"] } _ => &["membership"], }, "m.room.create" => &["creator"], "m.room.join_rules" => match version { RoomVersionId::V8 | RoomVersionId::V9 | RoomVersionId::V10 => &["join_rule", "allow"], _ => &["join_rule"], }, "m.room.power_levels" => &[ "ban", "events", "events_default", "kick", "redact", "state_default", "users", "users_default", ], "m.room.aliases" => match version { RoomVersionId::V1 | RoomVersionId::V2 | RoomVersionId::V3 | RoomVersionId::V4 | RoomVersionId::V5 => &["aliases"], // All other room versions, including custom ones, are treated by version 6 rules. // TODO: Should we return an error for unknown versions instead? _ => &[], }, #[cfg(feature = "unstable-msc2870")] "m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => { &["allow", "deny", "allow_ip_literals"] } "m.room.history_visibility" => &["history_visibility"], _ => &[], } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use js_int::int; use serde_json::{from_str as from_json_str, json, to_string as to_json_string}; use super::{to_canonical_value, try_from_json_map, value::CanonicalJsonValue}; #[test] fn serialize_canon() { let json: CanonicalJsonValue = json!({ "a": [1, 2, 3], "other": { "stuff": "hello" }, "string": "Thing" }) .try_into() .unwrap(); let ser = to_json_string(&json).unwrap(); let back = from_json_str::(&ser).unwrap(); assert_eq!(json, back); } #[test] fn check_canonical_sorts_keys() { let json: CanonicalJsonValue = json!({ "auth": { "success": true, "mxid": "@john.doe:example.com", "profile": { "display_name": "John Doe", "three_pids": [ { "medium": "email", "address": "john.doe@example.org" }, { "medium": "msisdn", "address": "123456789" } ] } } }) .try_into() .unwrap(); assert_eq!( to_json_string(&json).unwrap(), r#"{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}"# ); } #[test] fn serialize_map_to_canonical() { let mut expected = BTreeMap::new(); expected.insert("foo".into(), CanonicalJsonValue::String("string".into())); expected.insert( "bar".into(), CanonicalJsonValue::Array(vec![ CanonicalJsonValue::Integer(int!(0)), CanonicalJsonValue::Integer(int!(1)), CanonicalJsonValue::Integer(int!(2)), ]), ); let mut map = serde_json::Map::new(); map.insert("foo".into(), json!("string")); map.insert("bar".into(), json!(vec![0, 1, 2,])); assert_eq!(try_from_json_map(map).unwrap(), expected); } #[test] fn to_canonical() { #[derive(Debug, serde::Serialize)] struct Thing { foo: String, bar: Vec, } let t = Thing { foo: "string".into(), bar: vec![0, 1, 2] }; let mut expected = BTreeMap::new(); expected.insert("foo".into(), CanonicalJsonValue::String("string".into())); expected.insert( "bar".into(), CanonicalJsonValue::Array(vec![ CanonicalJsonValue::Integer(int!(0)), CanonicalJsonValue::Integer(int!(1)), CanonicalJsonValue::Integer(int!(2)), ]), ); assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected)); } } ruma-common-0.10.5/src/directory/filter_room_type_serde.rs000064400000000000000000000010701046102023000220440ustar 00000000000000use std::borrow::Cow; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::RoomTypeFilter; impl Serialize for RoomTypeFilter { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.as_str().serialize(serializer) } } impl<'de> Deserialize<'de> for RoomTypeFilter { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = Option::>::deserialize(deserializer)?; Ok(s.into()) } } ruma-common-0.10.5/src/directory/room_network_serde.rs000064400000000000000000000053001046102023000212070ustar 00000000000000use std::fmt; use serde::{ de::{Error, MapAccess, Visitor}, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer, }; use serde_json::Value as JsonValue; use super::{IncomingRoomNetwork, RoomNetwork}; impl<'a> Serialize for RoomNetwork<'a> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut state; match self { Self::Matrix => { state = serializer.serialize_struct("RoomNetwork", 0)?; } Self::All => { state = serializer.serialize_struct("RoomNetwork", 1)?; state.serialize_field("include_all_networks", &true)?; } Self::ThirdParty(network) => { state = serializer.serialize_struct("RoomNetwork", 1)?; state.serialize_field("third_party_instance_id", network)?; } } state.end() } } impl<'de> Deserialize<'de> for IncomingRoomNetwork { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_map(RoomNetworkVisitor) } } struct RoomNetworkVisitor; impl<'de> Visitor<'de> for RoomNetworkVisitor { type Value = IncomingRoomNetwork; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("Network selection") } fn visit_map(self, mut access: M) -> Result where M: MapAccess<'de>, { let mut include_all_networks = false; let mut third_party_instance_id = None; while let Some((key, value)) = access.next_entry::()? { match key.as_str() { "include_all_networks" => { include_all_networks = match value.as_bool() { Some(b) => b, _ => false, } } "third_party_instance_id" => { third_party_instance_id = value.as_str().map(|v| v.to_owned()); } _ => {} }; } if include_all_networks { if third_party_instance_id.is_none() { Ok(IncomingRoomNetwork::All) } else { Err(M::Error::custom( "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive.", )) } } else { Ok(match third_party_instance_id { Some(network) => IncomingRoomNetwork::ThirdParty(network), None => IncomingRoomNetwork::Matrix, }) } } } ruma-common-0.10.5/src/directory.rs000064400000000000000000000307401046102023000153060ustar 00000000000000//! Common types for room directory endpoints. use js_int::UInt; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3827")] mod filter_room_type_serde; mod room_network_serde; #[cfg(feature = "unstable-msc3827")] use crate::room::RoomType; use crate::{ serde::{Incoming, StringEnum}, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr, }; /// A chunk of a room list response, describing one room. /// /// To create an instance of this type, first create a `PublicRoomsChunkInit` and convert it via /// `PublicRoomsChunk::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PublicRoomsChunk { /// The canonical alias of the room, if any. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr( feature = "compat", serde(default, deserialize_with = "crate::serde::empty_string_as_none") )] pub canonical_alias: Option, /// The name of the room, if any. #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, /// The number of members joined to the room. pub num_joined_members: UInt, /// The ID of the room. pub room_id: OwnedRoomId, /// The topic of the room, if any. #[serde(skip_serializing_if = "Option::is_none")] pub topic: Option, /// Whether the room may be viewed by guest users without joining. pub world_readable: bool, /// Whether guest users may join the room and participate in it. /// /// If they can, they will be subject to ordinary power level rules like any other user. pub guest_can_join: bool, /// The URL for the room's avatar, if one is set. /// /// If you activate the `compat` feature, this field being an empty string in JSON will result /// in `None` here during deserialization. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr( feature = "compat", serde(default, deserialize_with = "crate::serde::empty_string_as_none") )] pub avatar_url: Option, /// The join rule of the room. #[serde(default, skip_serializing_if = "crate::serde::is_default")] pub join_rule: PublicRoomJoinRule, /// The type of room from `m.room.create`, if any. /// /// This field uses the unstable prefix from [MSC3827]. /// /// [MSC3827]: https://github.com/matrix-org/matrix-spec-proposals/pull/3827 #[cfg(feature = "unstable-msc3827")] #[serde( rename = "org.matrix.msc3827.room_type", alias = "room_type", skip_serializing_if = "Option::is_none" )] pub room_type: Option, } /// Initial set of mandatory fields of `PublicRoomsChunk`. /// /// This struct will not be updated even if additional fields are added to `PublicRoomsChunk` in a /// new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct PublicRoomsChunkInit { /// The number of members joined to the room. pub num_joined_members: UInt, /// The ID of the room. pub room_id: OwnedRoomId, /// Whether the room may be viewed by guest users without joining. pub world_readable: bool, /// Whether guest users may join the room and participate in it. /// /// If they can, they will be subject to ordinary power level rules like any other user. pub guest_can_join: bool, } impl From for PublicRoomsChunk { fn from(init: PublicRoomsChunkInit) -> Self { let PublicRoomsChunkInit { num_joined_members, room_id, world_readable, guest_can_join } = init; Self { canonical_alias: None, name: None, num_joined_members, room_id, topic: None, world_readable, guest_can_join, avatar_url: None, join_rule: PublicRoomJoinRule::default(), #[cfg(feature = "unstable-msc3827")] room_type: None, } } } /// A filter for public rooms lists #[derive(Clone, Debug, Default, Incoming, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[incoming_derive(Default)] pub struct Filter<'a> { /// A string to search for in the room metadata, e.g. name, topic, canonical alias etc. #[serde(skip_serializing_if = "Option::is_none")] pub generic_search_term: Option<&'a str>, /// The room types to include in the results. /// /// Includes all room types if it is empty. /// /// This field uses the unstable prefix from [MSC3827]. /// /// [MSC3827]: https://github.com/matrix-org/matrix-spec-proposals/pull/3827 #[cfg(feature = "unstable-msc3827")] #[serde( rename = "org.matrix.msc3827.room_types", alias = "room_types", default, skip_serializing_if = "Vec::is_empty" )] pub room_types: Vec, } impl Filter<'_> { /// Creates an empty `Filter`. pub fn new() -> Self { Default::default() } /// Returns `true` if the filter is empty. pub fn is_empty(&self) -> bool { self.generic_search_term.is_none() } } /// Information about which networks/protocols from application services on the /// homeserver from which to request rooms. #[derive(Clone, Debug, PartialEq, Eq, Incoming)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[incoming_derive(Clone, PartialEq, Eq, !Deserialize)] pub enum RoomNetwork<'a> { /// Return rooms from the Matrix network. Matrix, /// Return rooms from all the networks/protocols the homeserver knows about. All, /// Return rooms from a specific third party network/protocol. ThirdParty(&'a str), } impl<'a> Default for RoomNetwork<'a> { fn default() -> Self { RoomNetwork::Matrix } } /// The rule used for users wishing to join a public room. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum PublicRoomJoinRule { /// Users can request an invite to the room. Knock, /// Anyone can join the room without any prior action. Public, #[doc(hidden)] _Custom(PrivOwnedStr), } impl Default for PublicRoomJoinRule { fn default() -> Self { Self::Public } } /// An enum of possible room types to filter. /// /// This type can hold an arbitrary string. To build this with a custom value, convert it from an /// `Option` with `::from()` / `.into()`. [`RoomTypeFilter::Default`] can be constructed /// from `None`. /// /// To check for values that are not available as a documented variant here, use its string /// representation, obtained through [`.as_str()`](Self::as_str()). #[cfg(feature = "unstable-msc3827")] #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum RoomTypeFilter { /// The default room type, defined without a `room_type`. Default, /// A space. Space, /// A custom room type. #[doc(hidden)] _Custom(PrivOwnedStr), } #[cfg(feature = "unstable-msc3827")] impl RoomTypeFilter { /// Get the string representation of this `RoomTypeFilter`. /// /// [`RoomTypeFilter::Default`] returns `None`. pub fn as_str(&self) -> Option<&str> { match self { RoomTypeFilter::Default => None, RoomTypeFilter::Space => Some("m.space"), RoomTypeFilter::_Custom(s) => Some(&s.0), } } } #[cfg(feature = "unstable-msc3827")] impl From> for RoomTypeFilter where T: AsRef + Into>, { fn from(s: Option) -> Self { match s { None => Self::Default, Some(s) => match s.as_ref() { "m.space" => Self::Space, _ => Self::_Custom(PrivOwnedStr(s.into())), }, } } } #[cfg(test)] mod tests { #[cfg(feature = "unstable-msc3827")] use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[cfg(feature = "unstable-msc3827")] use super::RoomTypeFilter; use super::{Filter, IncomingFilter, IncomingRoomNetwork, RoomNetwork}; #[test] fn serialize_matrix_network_only() { let json = json!({}); assert_eq!(to_json_value(RoomNetwork::Matrix).unwrap(), json); } #[test] fn deserialize_matrix_network_only() { let json = json!({ "include_all_networks": false }); assert_eq!( from_json_value::(json).unwrap(), IncomingRoomNetwork::Matrix ); } #[test] fn serialize_default_network_is_empty() { let json = json!({}); assert_eq!(to_json_value(RoomNetwork::default()).unwrap(), json); } #[test] fn deserialize_empty_network_is_default() { let json = json!({}); assert_eq!( from_json_value::(json).unwrap(), IncomingRoomNetwork::Matrix ); } #[test] fn serialize_include_all_networks() { let json = json!({ "include_all_networks": true }); assert_eq!(to_json_value(RoomNetwork::All).unwrap(), json); } #[test] fn deserialize_include_all_networks() { let json = json!({ "include_all_networks": true }); assert_eq!(from_json_value::(json).unwrap(), IncomingRoomNetwork::All); } #[test] fn serialize_third_party_network() { let json = json!({ "third_party_instance_id": "freenode" }); assert_eq!(to_json_value(RoomNetwork::ThirdParty("freenode")).unwrap(), json); } #[test] fn deserialize_third_party_network() { let json = json!({ "third_party_instance_id": "freenode" }); assert_eq!( from_json_value::(json).unwrap(), IncomingRoomNetwork::ThirdParty("freenode".into()) ); } #[test] fn deserialize_include_all_networks_and_third_party_exclusivity() { let json = json!({ "include_all_networks": true, "third_party_instance_id": "freenode" }); assert_eq!( from_json_value::(json).unwrap_err().to_string().as_str(), "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive." ); } #[test] fn serialize_filter_empty() { let filter = Filter::default(); let json = json!({}); assert_eq!(to_json_value(filter).unwrap(), json); } #[test] fn deserialize_filter_empty() { let json = json!({}); let filter = from_json_value::(json).unwrap(); assert_eq!(filter.generic_search_term, None); #[cfg(feature = "unstable-msc3827")] assert_eq!(filter.room_types.len(), 0); } #[cfg(feature = "unstable-msc3827")] #[test] fn serialize_filter_room_types() { let filter = Filter { generic_search_term: None, room_types: vec![ RoomTypeFilter::Default, RoomTypeFilter::Space, Some("custom_type").into(), ], }; let json = json!({ "org.matrix.msc3827.room_types": [null, "m.space", "custom_type"] }); assert_eq!(to_json_value(filter).unwrap(), json); } #[cfg(feature = "unstable-msc3827")] #[test] fn deserialize_filter_room_types_unstable() { let json = json!({ "org.matrix.msc3827.room_types": [null, "m.space", "custom_type"] }); let filter = from_json_value::(json).unwrap(); assert_eq!(filter.room_types.len(), 3); assert_eq!(filter.room_types[0], RoomTypeFilter::Default); assert_eq!(filter.room_types[1], RoomTypeFilter::Space); assert_matches!(filter.room_types[2], RoomTypeFilter::_Custom(_)); assert_eq!(filter.room_types[2].as_str(), Some("custom_type")); } #[cfg(feature = "unstable-msc3827")] #[test] fn deserialize_filter_room_types_stable() { let json = json!({ "room_types": [null, "m.space", "custom_type"] }); let filter = from_json_value::(json).unwrap(); assert_eq!(filter.room_types.len(), 3); assert_eq!(filter.room_types[0], RoomTypeFilter::Default); assert_eq!(filter.room_types[1], RoomTypeFilter::Space); assert_matches!(filter.room_types[2], RoomTypeFilter::_Custom(_)); assert_eq!(filter.room_types[2].as_str(), Some("custom_type")); } } ruma-common-0.10.5/src/doc/rich_reply.md000064400000000000000000000016451046102023000161650ustar 00000000000000 This function requires an [`OriginalRoomMessageEvent`] since it creates a permalink to the previous message, for which the room ID is required. If you want to reply to an [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. If the message was edited, the previous message should be the original message that was edited, with the content of its replacement, to allow the fallback to be accurate at the time it is added. It is recommended to enable the `sanitize` feature when using this method as this will clean up nested [rich reply fallbacks] in chains of replies. This uses [`sanitize_html()`] internally, with [`RemoveReplyFallback::Yes`]. [rich reply fallbacks]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies ruma-common-0.10.5/src/doc/string_enum.md000064400000000000000000000005511046102023000163520ustar 00000000000000 This type can hold an arbitrary string. To build this with a custom value, convert it from a string with `::from()` / `.into()`. To check for values that are not available as a documented variant here, use its string representation, obtained through [`.as_str()`](Self::as_str()). ruma-common-0.10.5/src/encryption.rs000064400000000000000000000130371046102023000154740ustar 00000000000000//! Common types for [encryption] related tasks. //! //! [encryption]: https://spec.matrix.org/v1.2/client-server-api/#end-to-end-encryption use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::{ serde::{Base64, StringEnum}, EventEncryptionAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedUserId, PrivOwnedStr, }; /// Identity keys for a device. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct DeviceKeys { /// The ID of the user the device belongs to. /// /// Must match the user ID used when logging in. pub user_id: OwnedUserId, /// The ID of the device these keys belong to. /// /// Must match the device ID used when logging in. pub device_id: OwnedDeviceId, /// The encryption algorithms supported by this device. pub algorithms: Vec, /// Public identity keys. pub keys: BTreeMap, /// Signatures for the device key object. pub signatures: BTreeMap>, /// Additional data added to the device key information by intermediate servers, and /// not covered by the signatures. #[serde(default, skip_serializing_if = "UnsignedDeviceInfo::is_empty")] pub unsigned: UnsignedDeviceInfo, } impl DeviceKeys { /// Creates a new `DeviceKeys` from the given user id, device id, algorithms, keys and /// signatures. pub fn new( user_id: OwnedUserId, device_id: OwnedDeviceId, algorithms: Vec, keys: BTreeMap, signatures: BTreeMap>, ) -> Self { Self { user_id, device_id, algorithms, keys, signatures, unsigned: Default::default() } } } /// Additional data added to device key information by intermediate servers. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct UnsignedDeviceInfo { /// The display name which the user set on the device. #[serde(skip_serializing_if = "Option::is_none")] pub device_display_name: Option, } impl UnsignedDeviceInfo { /// Creates an empty `UnsignedDeviceInfo`. pub fn new() -> Self { Default::default() } /// Checks whether all fields are empty / `None`. pub fn is_empty(&self) -> bool { self.device_display_name.is_none() } } /// Signatures for a `SignedKey` object. pub type SignedKeySignatures = BTreeMap>; /// A key for the SignedCurve25519 algorithm #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SignedKey { /// Base64-encoded 32-byte Curve25519 public key. pub key: Base64, /// Signatures for the key object. pub signatures: SignedKeySignatures, /// Is this key considered to be a fallback key, defaults to false. #[serde(default, skip_serializing_if = "crate::serde::is_default")] pub fallback: bool, } impl SignedKey { /// Creates a new `SignedKey` with the given key and signatures. pub fn new(key: Base64, signatures: SignedKeySignatures) -> Self { Self { key, signatures, fallback: false } } /// Creates a new fallback `SignedKey` with the given key and signatures. pub fn new_fallback(key: Base64, signatures: SignedKeySignatures) -> Self { Self { key, signatures, fallback: true } } } /// A one-time public key for "pre-key" messages. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(untagged)] pub enum OneTimeKey { /// A key containing signatures, for the SignedCurve25519 algorithm. SignedKey(SignedKey), /// A string-valued key, for the Ed25519 and Curve25519 algorithms. Key(String), } /// Signatures for a `CrossSigningKey` object. pub type CrossSigningKeySignatures = BTreeMap>; /// A cross signing key. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct CrossSigningKey { /// The ID of the user the key belongs to. pub user_id: OwnedUserId, /// What the key is used for. pub usage: Vec, /// The public key. /// /// The object must have exactly one property. pub keys: BTreeMap, /// Signatures of the key. /// /// Only optional for master key. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub signatures: CrossSigningKeySignatures, } impl CrossSigningKey { /// Creates a new `CrossSigningKey` with the given user ID, usage, keys and signatures. pub fn new( user_id: OwnedUserId, usage: Vec, keys: BTreeMap, signatures: CrossSigningKeySignatures, ) -> Self { Self { user_id, usage, keys, signatures } } } /// The usage of a cross signing key. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, StringEnum)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_enum(rename_all = "snake_case")] pub enum KeyUsage { /// Master key. Master, /// Self-signing key. SelfSigning, /// User-signing key. UserSigning, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/events/_custom.rs000064400000000000000000000061561046102023000162630ustar 00000000000000use serde::Serialize; use serde_json::value::RawValue as RawJsonValue; use super::{ EphemeralRoomEventContent, EphemeralRoomEventType, EventContent, GlobalAccountDataEventContent, GlobalAccountDataEventType, HasDeserializeFields, MessageLikeEventContent, MessageLikeEventType, RedactContent, RedactedEventContent, RedactedMessageLikeEventContent, RedactedStateEventContent, RoomAccountDataEventContent, RoomAccountDataEventType, StateEventContent, StateEventType, StateUnsigned, ToDeviceEventContent, ToDeviceEventType, }; use crate::RoomVersionId; macro_rules! custom_event_content { ($i:ident, $evt:ident) => { /// A custom event's type. Used for event enum `_Custom` variants. // FIXME: Serialize shouldn't be required here, but it's currently a supertrait of // EventContent #[derive(Clone, Debug, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct $i { #[serde(skip)] event_type: Box, } impl EventContent for $i { type EventType = $evt; fn event_type(&self) -> Self::EventType { self.event_type[..].into() } fn from_parts(event_type: &str, _content: &RawJsonValue) -> serde_json::Result { Ok(Self { event_type: event_type.into() }) } } }; } macro_rules! custom_room_event_content { ($i:ident, $evt:ident) => { custom_event_content!($i, $evt); impl RedactContent for $i { type Redacted = Self; fn redact(self, _: &RoomVersionId) -> Self { self } } impl RedactedEventContent for $i { fn empty(event_type: &str) -> serde_json::Result { Ok(Self { event_type: event_type.into() }) } fn has_serialize_fields(&self) -> bool { false } fn has_deserialize_fields() -> HasDeserializeFields { HasDeserializeFields::False } } }; } custom_event_content!(CustomGlobalAccountDataEventContent, GlobalAccountDataEventType); impl GlobalAccountDataEventContent for CustomGlobalAccountDataEventContent {} custom_event_content!(CustomRoomAccountDataEventContent, RoomAccountDataEventType); impl RoomAccountDataEventContent for CustomRoomAccountDataEventContent {} custom_event_content!(CustomEphemeralRoomEventContent, EphemeralRoomEventType); impl EphemeralRoomEventContent for CustomEphemeralRoomEventContent {} custom_room_event_content!(CustomMessageLikeEventContent, MessageLikeEventType); impl MessageLikeEventContent for CustomMessageLikeEventContent {} impl RedactedMessageLikeEventContent for CustomMessageLikeEventContent {} custom_room_event_content!(CustomStateEventContent, StateEventType); impl StateEventContent for CustomStateEventContent { type StateKey = String; type Unsigned = StateUnsigned; } impl RedactedStateEventContent for CustomStateEventContent {} custom_event_content!(CustomToDeviceEventContent, ToDeviceEventType); impl ToDeviceEventContent for CustomToDeviceEventContent {} ruma-common-0.10.5/src/events/audio/amplitude_serde.rs000064400000000000000000000006411046102023000210520ustar 00000000000000//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). use js_int::UInt; use serde::Deserialize; use super::Amplitude; impl<'de> Deserialize<'de> for Amplitude { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let uint = UInt::deserialize(deserializer)?; Ok(Self(uint.min(Self::MAX.into()))) } } ruma-common-0.10.5/src/events/audio/waveform_serde.rs000064400000000000000000000007111046102023000207120ustar 00000000000000//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). use serde::Deserialize; use super::{Amplitude, Waveform, WaveformError}; #[derive(Debug, Default, Deserialize)] pub(crate) struct WaveformSerDeHelper(Vec); impl TryFrom for Waveform { type Error = WaveformError; fn try_from(helper: WaveformSerDeHelper) -> Result { Waveform::try_from(helper.0) } } ruma-common-0.10.5/src/events/audio.rs000064400000000000000000000165361046102023000157160ustar 00000000000000//! Types for extensible audio message events ([MSC3246]). //! //! [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246 use std::time::Duration; use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; mod amplitude_serde; mod waveform_serde; use waveform_serde::WaveformSerDeHelper; use super::{ file::FileContent, message::MessageContent, room::message::{ AudioInfo, AudioMessageEventContent, MessageType, Relation, RoomMessageEventContent, }, }; /// The payload for an extensible audio message. /// /// This is the new primary type introduced in [MSC3246] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `AudioEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Audio`]. You can convert it back with /// [`AudioEventContent::from_audio_room_message()`]. /// /// [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Audio`]: super::room::message::MessageType::Audio #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.audio", kind = MessageLike)] pub struct AudioEventContent { /// The text representation of the message. #[serde(flatten)] pub message: MessageContent, /// The file content of the message. #[serde(rename = "m.file")] pub file: FileContent, /// The audio content of the message. #[serde(rename = "m.audio")] pub audio: AudioContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl AudioEventContent { /// Creates a new `AudioEventContent` with the given plain text message and file. pub fn plain(message: impl Into, file: FileContent) -> Self { Self { message: MessageContent::plain(message), file, audio: Default::default(), relates_to: None, } } /// Creates a new `AudioEventContent` with the given message and file. pub fn with_message(message: MessageContent, file: FileContent) -> Self { Self { message, file, audio: Default::default(), relates_to: None } } /// Create a new `AudioEventContent` from the given `AudioMessageEventContent` and optional /// relation. pub fn from_audio_room_message( content: AudioMessageEventContent, relates_to: Option, ) -> Self { // Otherwise the `voice` field has an extra indentation #[rustfmt::skip] let AudioMessageEventContent { body, source, info, message, file, audio, #[cfg(feature = "unstable-msc3245")] voice: _, } = content; let AudioInfo { duration, mimetype, size } = info.map(|info| *info).unwrap_or_default(); let message = message.unwrap_or_else(|| MessageContent::plain(body)); let file = file.unwrap_or_else(|| { FileContent::from_room_message_content(source, None, mimetype, size) }); let audio = audio.unwrap_or_else(|| { let mut content = AudioContent::new(); content.duration = duration; content }); Self { message, file, audio, relates_to } } } impl From for RoomMessageEventContent { fn from(content: AudioEventContent) -> Self { let AudioEventContent { message, file, audio, relates_to } = content; Self { msgtype: MessageType::Audio(AudioMessageEventContent::from_extensible_content( message, file, audio, )), relates_to, } } } /// Audio content. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct AudioContent { /// The duration of the video in milliseconds. #[serde( with = "ruma_common::serde::duration::opt_ms", default, skip_serializing_if = "Option::is_none" )] pub duration: Option, /// The waveform representation of the audio content. #[serde(default, skip_serializing_if = "Option::is_none")] pub waveform: Option, } impl AudioContent { /// Creates a new empty `AudioContent`. pub fn new() -> Self { Self::default() } /// Creates a new `AudioContent` with the given duration. pub(crate) fn from_room_message_content(duration: Duration) -> Self { Self { duration: Some(duration), ..Default::default() } } /// Whether this `AudioContent` is empty. pub fn is_empty(&self) -> bool { self.duration.is_none() && self.waveform.is_none() } } /// The waveform representation of audio content. /// /// Must include between 30 and 120 `Amplitude`s. /// /// To build this, use the `TryFrom` implementations. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "WaveformSerDeHelper")] pub struct Waveform(Vec); impl Waveform { /// The smallest number of values contained in a `Waveform`. pub const MIN_LENGTH: usize = 30; /// The largest number of values contained in a `Waveform`. pub const MAX_LENGTH: usize = 120; /// The amplitudes of this `Waveform`. pub fn amplitudes(&self) -> &[Amplitude] { &self.0 } } /// An error encountered when trying to convert to a `Waveform`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum WaveformError { /// There are more than [`Waveform::MAX_LENGTH`] values. #[error("too many values")] TooManyValues, /// There are less that [`Waveform::MIN_LENGTH`] values. #[error("not enough values")] NotEnoughValues, } impl TryFrom> for Waveform { type Error = WaveformError; fn try_from(value: Vec) -> Result { if value.len() < Self::MIN_LENGTH { Err(WaveformError::NotEnoughValues) } else if value.len() > Self::MAX_LENGTH { Err(WaveformError::TooManyValues) } else { Ok(Self(value)) } } } impl TryFrom<&[Amplitude]> for Waveform { type Error = WaveformError; fn try_from(value: &[Amplitude]) -> Result { Self::try_from(value.to_owned()) } } /// The amplitude of a waveform sample. /// /// Must be an integer between 0 and 1024. #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct Amplitude(UInt); impl Amplitude { /// The smallest value that can be represented by this type, 0. pub const MIN: u16 = 0; /// The largest value that can be represented by this type, 1024. pub const MAX: u16 = 1024; /// Creates a new `Amplitude` with the given value. /// /// It will saturate if it is bigger than [`Amplitude::MAX`]. pub fn new(value: u16) -> Self { Self(value.min(Self::MAX).into()) } /// The value of this `Amplitude`. pub fn get(&self) -> UInt { self.0 } } impl From for Amplitude { fn from(value: u16) -> Self { Self::new(value) } } ruma-common-0.10.5/src/events/call/answer.rs000064400000000000000000000051521046102023000170170ustar 00000000000000//! Types for the [`m.call.answer`] event. //! //! [`m.call.answer`]: https://spec.matrix.org/v1.2/client-server-api/#mcallanswer use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::AnswerSessionDescription; #[cfg(feature = "unstable-msc2746")] use super::CallCapabilities; use crate::{OwnedVoipId, VoipVersionId}; /// The content of an `m.call.answer` event. /// /// This event is sent by the callee when they wish to answer the call. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.answer", kind = MessageLike)] pub struct CallAnswerEventContent { /// The VoIP session description object. pub answer: AnswerSessionDescription, /// A unique identifier for the call. pub call_id: OwnedVoipId, #[cfg(feature = "unstable-msc2746")] /// **Required in VoIP version 1.** A unique ID for this session for the duration of the call. #[serde(skip_serializing_if = "Option::is_none")] pub party_id: Option, /// The version of the VoIP specification this messages adheres to. pub version: VoipVersionId, #[cfg(feature = "unstable-msc2746")] /// **Added in VoIP version 1.** The VoIP capabilities of the client. #[serde(default, skip_serializing_if = "CallCapabilities::is_default")] pub capabilities: CallCapabilities, } impl CallAnswerEventContent { /// Creates an `CallAnswerEventContent` with the given answer, call ID and VoIP version. pub fn new( answer: AnswerSessionDescription, call_id: OwnedVoipId, version: VoipVersionId, ) -> Self { Self { answer, call_id, #[cfg(feature = "unstable-msc2746")] party_id: None, version, #[cfg(feature = "unstable-msc2746")] capabilities: Default::default(), } } /// Convenience method to create a VoIP version 0 `CallAnswerEventContent` with all the required /// fields. pub fn version_0(answer: AnswerSessionDescription, call_id: OwnedVoipId) -> Self { Self::new(answer, call_id, VoipVersionId::V0) } /// Convenience method to create a VoIP version 1 `CallAnswerEventContent` with all the required /// fields. #[cfg(feature = "unstable-msc2746")] pub fn version_1( answer: AnswerSessionDescription, call_id: OwnedVoipId, party_id: OwnedVoipId, capabilities: CallCapabilities, ) -> Self { Self { answer, call_id, party_id: Some(party_id), version: VoipVersionId::V1, capabilities } } } ruma-common-0.10.5/src/events/call/candidates.rs000064400000000000000000000064741046102023000176270ustar 00000000000000//! Types for the [`m.call.candidates`] event. //! //! [`m.call.candidates`]: https://spec.matrix.org/v1.2/client-server-api/#mcallcandidates use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{OwnedVoipId, VoipVersionId}; /// The content of an `m.call.candidates` event. /// /// This event is sent by callers after sending an invite and by the callee after answering. Its /// purpose is to give the other party additional ICE candidates to try using to communicate. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.candidates", kind = MessageLike)] pub struct CallCandidatesEventContent { /// A unique identifier for the call. pub call_id: OwnedVoipId, #[cfg(feature = "unstable-msc2746")] /// **Required in VoIP version 1.** The unique ID for this session for the duration of the /// call. /// /// Must be the same as the one sent by the previous invite or answer from /// this session. #[serde(skip_serializing_if = "Option::is_none")] pub party_id: Option, /// A list of candidates. /// /// With the `unstable-msc2746` feature, in VoIP version 1, this list should end with a /// `Candidate` with an empty `candidate` field when no more candidates will be sent. pub candidates: Vec, /// The version of the VoIP specification this messages adheres to. pub version: VoipVersionId, } impl CallCandidatesEventContent { /// Creates a new `CallCandidatesEventContent` with the given call id, candidate list and VoIP /// version. pub fn new(call_id: OwnedVoipId, candidates: Vec, version: VoipVersionId) -> Self { Self { call_id, candidates, version, #[cfg(feature = "unstable-msc2746")] party_id: None, } } /// Convenience method to create a VoIP version 0 `CallCandidatesEventContent` with all the /// required fields. pub fn version_0(call_id: OwnedVoipId, candidates: Vec) -> Self { Self::new(call_id, candidates, VoipVersionId::V0) } /// Convenience method to create a VoIP version 1 `CallCandidatesEventContent` with all the /// required fields. #[cfg(feature = "unstable-msc2746")] pub fn version_1( call_id: OwnedVoipId, party_id: OwnedVoipId, candidates: Vec, ) -> Self { Self { call_id, party_id: Some(party_id), candidates, version: VoipVersionId::V1 } } } /// An ICE (Interactive Connectivity Establishment) candidate. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(rename_all = "camelCase")] pub struct Candidate { /// The SDP "a" line of the candidate. pub candidate: String, /// The SDP media type this candidate is intended for. pub sdp_mid: String, /// The index of the SDP "m" line this candidate is intended for. pub sdp_m_line_index: UInt, } impl Candidate { /// Creates a new `Candidate` with the given "a" line, SDP media type and SDP "m" line. pub fn new(candidate: String, sdp_mid: String, sdp_m_line_index: UInt) -> Self { Self { candidate, sdp_mid, sdp_m_line_index } } } ruma-common-0.10.5/src/events/call/hangup.rs000064400000000000000000000123111046102023000167750ustar 00000000000000//! Types for the [`m.call.hangup`] event. //! //! [`m.call.hangup`]: https://spec.matrix.org/v1.2/client-server-api/#mcallhangup use ruma_macros::EventContent; #[cfg(feature = "unstable-msc2746")] use serde::Serializer; use serde::{Deserialize, Serialize}; use crate::{serde::StringEnum, OwnedVoipId, PrivOwnedStr, VoipVersionId}; /// The content of an `m.call.hangup` event. /// /// Sent by either party to signal their termination of the call. /// /// In VoIP version 0, this can be sent either once the call has been established or before to abort /// the call. /// /// With the `unstable-msc2746` feature, and if the call is using VoIP version 1, this should only /// be sent by the caller after sending the invite or by the callee after answering the invite. To /// reject an invite, send an [`m.call.reject`] event. /// /// [`m.call.reject`]: super::reject::CallRejectEventContent #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.hangup", kind = MessageLike)] pub struct CallHangupEventContent { /// A unique identifier for the call. pub call_id: OwnedVoipId, #[cfg(feature = "unstable-msc2746")] /// **Required in VoIP version 1.** A unique ID for this session for the duration of the call. /// /// Must be the same as the one sent by the previous invite or answer from /// this session. #[serde(skip_serializing_if = "Option::is_none")] pub party_id: Option, /// The version of the VoIP specification this messages adheres to. pub version: VoipVersionId, /// Optional error reason for the hangup. /// /// With the `unstable-msc2746` feature, this field defaults to `Some(Reason::UserHangup)`. #[cfg_attr(not(feature = "unstable-msc2746"), serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr( feature = "unstable-msc2746", serde( default = "Reason::option_with_default", serialize_with = "Reason::serialize_option_with_default" ) )] pub reason: Option, } impl CallHangupEventContent { /// Creates a new `CallHangupEventContent` with the given call ID and VoIP version. pub fn new(call_id: OwnedVoipId, version: VoipVersionId) -> Self { Self { call_id, #[cfg(feature = "unstable-msc2746")] party_id: None, version, reason: Default::default(), } } /// Convenience method to create a VoIP version 0 `CallHangupEventContent` with all the required /// fields. pub fn version_0(call_id: OwnedVoipId) -> Self { Self::new(call_id, VoipVersionId::V0) } /// Convenience method to create a VoIP version 1 `CallHangupEventContent` with all the required /// fields. #[cfg(feature = "unstable-msc2746")] pub fn version_1(call_id: OwnedVoipId, party_id: OwnedVoipId, reason: Reason) -> Self { Self { call_id, party_id: Some(party_id), version: VoipVersionId::V1, reason: Some(reason) } } } /// A reason for a hangup. /// /// Should not be provided when the user naturally ends or rejects the call. When there was an error /// in the call negotiation, this should be `ice_failed` for when ICE negotiation fails or /// `invite_timeout` for when the other party did not answer in time. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum Reason { /// ICE negotiation failure. IceFailed, /// Party did not answer in time. InviteTimeout, /// The connection failed after some media was exchanged. /// /// Note that, in the case of an ICE renegotiation, a client should be sure to send /// `ice_timeout` rather than `ice_failed` if media had previously been received successfully, /// even if the ICE renegotiation itself failed. #[cfg(feature = "unstable-msc2746")] IceTimeout, /// The user chose to end the call. #[cfg(feature = "unstable-msc2746")] UserHangup, /// The client was unable to start capturing media in such a way as it is unable to continue /// the call. #[cfg(feature = "unstable-msc2746")] UserMediaFailed, /// The user is busy. #[cfg(feature = "unstable-msc2746")] UserBusy, /// Some other failure occurred that meant the client was unable to continue the call rather /// than the user choosing to end it. #[cfg(feature = "unstable-msc2746")] UnknownError, #[doc(hidden)] _Custom(PrivOwnedStr), } impl Reason { #[cfg(feature = "unstable-msc2746")] fn serialize_option_with_default( reason: &Option, serializer: S, ) -> Result where S: Serializer, { if let Some(reason) = &reason { reason.serialize(serializer) } else { Self::default().serialize(serializer) } } #[cfg(feature = "unstable-msc2746")] fn option_with_default() -> Option { Some(Self::default()) } } #[cfg(feature = "unstable-msc2746")] impl Default for Reason { fn default() -> Self { Self::UserHangup } } ruma-common-0.10.5/src/events/call/invite.rs000064400000000000000000000071651046102023000170240ustar 00000000000000//! Types for the [`m.call.invite`] event. //! //! [`m.call.invite`]: https://spec.matrix.org/v1.2/client-server-api/#mcallinvite use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc2746")] use super::CallCapabilities; use super::OfferSessionDescription; #[cfg(feature = "unstable-msc2746")] use crate::OwnedUserId; use crate::{OwnedVoipId, VoipVersionId}; /// The content of an `m.call.invite` event. /// /// This event is sent by the caller when they wish to establish a call. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.invite", kind = MessageLike)] pub struct CallInviteEventContent { /// A unique identifier for the call. pub call_id: OwnedVoipId, #[cfg(feature = "unstable-msc2746")] /// **Required in VoIP version 1.** A unique ID for this session for the duration of the call. #[serde(skip_serializing_if = "Option::is_none")] pub party_id: Option, /// The time in milliseconds that the invite is valid for. /// /// Once the invite age exceeds this value, clients should discard it. They should also no /// longer show the call as awaiting an answer in the UI. pub lifetime: UInt, /// The session description object. pub offer: OfferSessionDescription, /// The version of the VoIP specification this messages adheres to. pub version: VoipVersionId, #[cfg(feature = "unstable-msc2746")] /// **Added in VoIP version 1.** The VoIP capabilities of the client. #[serde(default, skip_serializing_if = "CallCapabilities::is_default")] pub capabilities: CallCapabilities, #[cfg(feature = "unstable-msc2746")] /// **Added in VoIP version 1.** The intended target of the invite, if any. /// /// If this is `None`, the invite is intended for any member of the room, except the sender. /// /// The invite should be ignored if the invitee is set and doesn't match the user's ID. #[serde(skip_serializing_if = "Option::is_none")] pub invitee: Option, } impl CallInviteEventContent { /// Creates a new `CallInviteEventContent` with the given call ID, lifetime, offer and VoIP /// version. pub fn new( call_id: OwnedVoipId, lifetime: UInt, offer: OfferSessionDescription, version: VoipVersionId, ) -> Self { Self { call_id, #[cfg(feature = "unstable-msc2746")] party_id: None, lifetime, offer, version, #[cfg(feature = "unstable-msc2746")] capabilities: Default::default(), #[cfg(feature = "unstable-msc2746")] invitee: None, } } /// Convenience method to create a version 0 `CallInviteEventContent` with all the required /// fields. pub fn version_0(call_id: OwnedVoipId, lifetime: UInt, offer: OfferSessionDescription) -> Self { Self::new(call_id, lifetime, offer, VoipVersionId::V0) } /// Convenience method to create a version 1 `CallInviteEventContent` with all the required /// fields. #[cfg(feature = "unstable-msc2746")] pub fn version_1( call_id: OwnedVoipId, party_id: OwnedVoipId, lifetime: UInt, offer: OfferSessionDescription, capabilities: CallCapabilities, ) -> Self { Self { call_id, party_id: Some(party_id), lifetime, offer, version: VoipVersionId::V1, capabilities, invitee: None, } } } ruma-common-0.10.5/src/events/call/negotiate.rs000064400000000000000000000033651046102023000175030ustar 00000000000000//! Types for the `m.call.negotiate` event [MSC2746]. //! //! [MSC2746]: https://github.com/matrix-org/matrix-spec-proposals/pull/2746 use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::SessionDescription; use crate::OwnedVoipId; /// **Added in VoIP version 1.** The content of an `m.call.negotiate` event. /// /// This event is sent by either party after the call is established to renegotiate it. It can be /// used for media pause, hold/resume, ICE restarts and voice/video call up/downgrading. /// /// First an event must be sent with an `offer` session description, which is replied to with an /// event with an `answer` session description. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.negotiate", kind = MessageLike)] pub struct CallNegotiateEventContent { /// The ID of the call this event relates to. pub call_id: OwnedVoipId, /// The unique ID for this session for the duration of the call. /// /// Must be the same as the one sent by the previous invite or answer from /// this session. pub party_id: OwnedVoipId, /// The time in milliseconds that the negotiation is valid for. pub lifetime: UInt, /// The session description of the negotiation. pub description: SessionDescription, } impl CallNegotiateEventContent { /// Creates a `CallNegotiateEventContent` with the given call ID, party ID, lifetime and /// description. pub fn new( call_id: OwnedVoipId, party_id: OwnedVoipId, lifetime: UInt, description: SessionDescription, ) -> Self { Self { call_id, party_id, lifetime, description } } } ruma-common-0.10.5/src/events/call/reject.rs000064400000000000000000000027401046102023000167740ustar 00000000000000//! Types for the `m.call.reject` event [MSC2746]. //! //! [MSC2746]: https://github.com/matrix-org/matrix-spec-proposals/pull/2746 use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{OwnedVoipId, VoipVersionId}; /// **Added in VoIP version 1.** The content of an `m.call.reject` event. /// /// Starting from VoIP version 1, this event is sent by the callee to reject an invite. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.reject", kind = MessageLike)] pub struct CallRejectEventContent { /// The ID of the call this event relates to. pub call_id: OwnedVoipId, /// A unique ID for this session for the duration of the call. pub party_id: OwnedVoipId, /// The version of the VoIP specification this messages adheres to. /// /// Cannot be older than `VoipVersionId::V1`. pub version: VoipVersionId, } impl CallRejectEventContent { /// Creates a `CallRejectEventContent` with the given call ID, VoIP version and party ID. pub fn new(call_id: OwnedVoipId, party_id: OwnedVoipId, version: VoipVersionId) -> Self { Self { call_id, party_id, version } } /// Convenience method to create a version 1 `CallRejectEventContent` with all the required /// fields. pub fn version_1(call_id: OwnedVoipId, party_id: OwnedVoipId) -> Self { Self::new(call_id, party_id, VoipVersionId::V1) } } ruma-common-0.10.5/src/events/call/select_answer.rs000064400000000000000000000036161046102023000203610ustar 00000000000000//! Types for the `m.call.select_answer` event [MSC2746]. //! //! [MSC2746]: https://github.com/matrix-org/matrix-spec-proposals/pull/2746 use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{OwnedVoipId, VoipVersionId}; /// **Added in VoIP version 1.** The content of an `m.call.select_answer` event. /// /// This event is sent by the caller when it has chosen an answer. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.call.select_answer", kind = MessageLike)] pub struct CallSelectAnswerEventContent { /// The ID of the call this event relates to. pub call_id: OwnedVoipId, /// A unique ID for this session for the duration of the call. /// /// Must be the same as the one sent by the previous invite from this session. pub party_id: OwnedVoipId, /// The party ID of the selected answer to the previously sent invite. pub selected_party_id: OwnedVoipId, /// The version of the VoIP specification this messages adheres to. /// /// Cannot be older than `VoipVersionId::V1`. pub version: VoipVersionId, } impl CallSelectAnswerEventContent { /// Creates a `CallSelectAnswerEventContent` with the given call ID, VoIP version, party ID and /// selected party ID. pub fn new( call_id: OwnedVoipId, party_id: OwnedVoipId, selected_party_id: OwnedVoipId, version: VoipVersionId, ) -> Self { Self { call_id, party_id, selected_party_id, version } } /// Convenience method to create a version 1 `CallSelectAnswerEventContent` with all the /// required fields. pub fn version_1( call_id: OwnedVoipId, party_id: OwnedVoipId, selected_party_id: OwnedVoipId, ) -> Self { Self::new(call_id, party_id, selected_party_id, VoipVersionId::V1) } } ruma-common-0.10.5/src/events/call.rs000064400000000000000000000101371046102023000155170ustar 00000000000000//! Modules for events in the `m.call` namespace. //! //! This module also contains types shared by events in its child namespaces. pub mod answer; pub mod candidates; pub mod hangup; pub mod invite; #[cfg(feature = "unstable-msc2746")] pub mod negotiate; #[cfg(feature = "unstable-msc2746")] pub mod reject; #[cfg(feature = "unstable-msc2746")] pub mod select_answer; use serde::{Deserialize, Serialize}; use crate::{serde::StringEnum, PrivOwnedStr}; /// A VoIP session description. /// /// This is the same type as WebRTC's [`RTCSessionDescriptionInit`]. /// /// [`RTCSessionDescriptionInit`]: (https://www.w3.org/TR/webrtc/#dom-rtcsessiondescriptioninit): #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SessionDescription { /// The type of session description. #[serde(rename = "type")] pub session_type: SessionDescriptionType, /// The SDP text of the session description. /// /// With the `unstable-msc2746` feature, this field is unused if the type is `rollback` and /// defaults to an empty string. #[cfg_attr(feature = "unstable-msc2746", serde(default))] pub sdp: String, } impl SessionDescription { /// Creates a new `SessionDescription` with the given session type and SDP text. pub fn new(session_type: SessionDescriptionType, sdp: String) -> Self { Self { session_type, sdp } } } /// The type of VoIP session description. /// /// This is the same type as WebRTC's [`RTCSdpType`]. /// /// [`RTCSdpType`]: (https://www.w3.org/TR/webrtc/#dom-rtcsdptype): #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "lowercase")] #[non_exhaustive] pub enum SessionDescriptionType { /// The description must be treated as an SDP final answer, and the offer-answer exchange must /// be considered complete. Answer, /// The description must be treated as an SDP offer. Offer, /// The description must be treated as an SDP answer, but not final. #[cfg(feature = "unstable-msc2746")] PrAnswer, /// The description must be treated as cancelling the current SDP negotiation and moving the /// SDP offer back to what it was in the previous stable state. #[cfg(feature = "unstable-msc2746")] Rollback, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A VoIP answer session description. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "answer")] pub struct AnswerSessionDescription { /// The SDP text of the session description. pub sdp: String, } impl AnswerSessionDescription { /// Creates a new `AnswerSessionDescription` with the given SDP text. pub fn new(sdp: String) -> Self { Self { sdp } } } /// A VoIP offer session description. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type", rename = "offer")] pub struct OfferSessionDescription { /// The SDP text of the session description. pub sdp: String, } impl OfferSessionDescription { /// Creates a new `OfferSessionDescription` with the given SDP text. pub fn new(sdp: String) -> Self { Self { sdp } } } /// The capabilities of a client in a VoIP call. #[cfg(feature = "unstable-msc2746")] #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct CallCapabilities { /// Whether this client supports [DTMF]. /// /// Defaults to `false`. /// /// [DTMF]: https://w3c.github.io/webrtc-pc/#peer-to-peer-dtmf #[serde(rename = "m.call.dtmf", default)] pub dtmf: bool, } #[cfg(feature = "unstable-msc2746")] impl CallCapabilities { /// Creates a default `CallCapabilities`. pub fn new() -> Self { Self::default() } /// Whether this `CallCapabilities` only contains default values. pub fn is_default(&self) -> bool { !self.dtmf } } ruma-common-0.10.5/src/events/content.rs000064400000000000000000000130361046102023000162570ustar 00000000000000use std::fmt; use serde::{de::DeserializeOwned, Serialize}; use serde_json::value::RawValue as RawJsonValue; use crate::serde::{CanBeEmpty, Raw}; use super::{ EphemeralRoomEventType, GlobalAccountDataEventType, MessageLikeEventType, RoomAccountDataEventType, StateEventType, StateUnsignedFromParts, ToDeviceEventType, }; /// The base trait that all event content types implement. /// /// Use [`macros::EventContent`] to derive this traits. It is not meant to be implemented manually. /// /// [`macros::EventContent`]: super::macros::EventContent pub trait EventContent: Sized + Serialize { /// The Rust enum for the event kind's known types. type EventType; /// Get the event's type, like `m.room.message`. fn event_type(&self) -> Self::EventType; /// Constructs the given event content. #[doc(hidden)] fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result; } impl Raw where T: EventContent, T::EventType: fmt::Display, { /// Try to deserialize the JSON as an event's content. pub fn deserialize_content(&self, event_type: T::EventType) -> serde_json::Result { T::from_parts(&event_type.to_string(), self.json()) } } /// The base trait that all redacted event content types implement. /// /// This trait's associated functions and methods should not be used to build /// redacted events, prefer the `redact` method on `AnyStateEvent` and /// `AnyMessageLikeEvent` and their "sync" and "stripped" counterparts. /// The `RedactedEventContent` trait is an implementation detail, ruma makes no /// API guarantees. pub trait RedactedEventContent: EventContent { /// Constructs the redacted event content. /// /// If called for anything but "empty" redacted content this will error. #[doc(hidden)] fn empty(_event_type: &str) -> serde_json::Result { Err(serde::de::Error::custom("this event is not redacted")) } /// Determines if the redacted event content needs to serialize fields. #[doc(hidden)] fn has_serialize_fields(&self) -> bool; /// Determines if the redacted event content needs to deserialize fields. #[doc(hidden)] fn has_deserialize_fields() -> HasDeserializeFields; } /// `HasDeserializeFields` is used in the code generated by the `Event` derive /// to aid in deserializing redacted events. #[doc(hidden)] #[derive(Debug)] #[allow(clippy::exhaustive_enums)] pub enum HasDeserializeFields { /// Deserialize the event's content, failing if invalid. True, /// Return the redacted version of this event's content. False, /// `Optional` is used for `RedactedAliasesEventContent` since it has /// an empty version and one with content left after redaction that /// must be supported together. Optional, } /// Trait for abstracting over event content structs. /// /// … but *not* enums which don't always have an event type and kind (e.g. message vs state) that's /// fixed / known at compile time. pub trait StaticEventContent: EventContent { /// The event's "kind". /// /// See the type's documentation. const KIND: EventKind; /// The event type. const TYPE: &'static str; } /// The "kind" of an event. /// /// This corresponds directly to the event content marker traits. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum EventKind { /// Global account data event kind. GlobalAccountData, /// Room account data event kind. RoomAccountData, /// Ephemeral room event kind. EphemeralRoomData, /// Message-like event kind. /// /// Since redacted / non-redacted message-like events are used in the same places but have /// different sets of fields, these two variations are treated as two closely-related event /// kinds. MessageLike { /// Redacted variation? redacted: bool, }, /// State event kind. /// /// Since redacted / non-redacted state events are used in the same places but have different /// sets of fields, these two variations are treated as two closely-related event kinds. State { /// Redacted variation? redacted: bool, }, /// To-device event kind. ToDevice, /// Presence event kind. Presence, } /// Content of a global account-data event. pub trait GlobalAccountDataEventContent: EventContent { } /// Content of a room-specific account-data event. pub trait RoomAccountDataEventContent: EventContent {} /// Content of an ephemeral room event. pub trait EphemeralRoomEventContent: EventContent {} /// Content of a non-redacted message-like event. pub trait MessageLikeEventContent: EventContent {} /// Content of a redacted message-like event. pub trait RedactedMessageLikeEventContent: MessageLikeEventContent + RedactedEventContent {} /// Content of a redacted state event. pub trait StateEventContent: EventContent { /// The type of the event's `state_key` field. type StateKey: AsRef + Clone + fmt::Debug + DeserializeOwned + Serialize; /// The type of the event's `unsigned` field. type Unsigned: Clone + fmt::Debug + Default + CanBeEmpty + StateUnsignedFromParts + Serialize; } /// Content of a non-redacted state event. pub trait RedactedStateEventContent: StateEventContent + RedactedEventContent {} /// Content of a to-device event. pub trait ToDeviceEventContent: EventContent {} ruma-common-0.10.5/src/events/direct.rs000064400000000000000000000050231046102023000160540ustar 00000000000000//! Types for the [`m.direct`] event. //! //! [`m.direct`]: https://spec.matrix.org/v1.2/client-server-api/#mdirect use std::{ collections::BTreeMap, ops::{Deref, DerefMut}, }; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{OwnedRoomId, OwnedUserId}; /// The content of an `m.direct` event. /// /// A mapping of `UserId`s to a list of `RoomId`s which are considered *direct* for that particular /// user. /// /// Informs the client about the rooms that are considered direct by a user. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] #[ruma_event(type = "m.direct", kind = GlobalAccountData)] pub struct DirectEventContent(pub BTreeMap>); impl Deref for DirectEventContent { type Target = BTreeMap>; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for DirectEventContent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[cfg(all(test, feature = "rand"))] mod tests { use std::collections::BTreeMap; use crate::{server_name, RoomId, UserId}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{DirectEvent, DirectEventContent}; #[test] fn serialization() { let mut content = DirectEventContent(BTreeMap::new()); let server_name = server_name!("ruma.io"); let alice = UserId::new(server_name); let room = vec![RoomId::new(server_name)]; content.insert(alice.clone(), room.clone()); let event = DirectEvent { content }; let json_data = json!({ "content": { alice.to_string(): vec![room[0].to_string()], }, "type": "m.direct" }); assert_eq!(to_json_value(&event).unwrap(), json_data); } #[test] fn deserialization() { let server_name = server_name!("ruma.io"); let alice = UserId::new(server_name); let rooms = vec![RoomId::new(server_name), RoomId::new(server_name)]; let json_data = json!({ "content": { alice.to_string(): vec![rooms[0].to_string(), rooms[1].to_string()], }, "type": "m.direct" }); let event: DirectEvent = from_json_value(json_data).unwrap(); let direct_rooms = event.content.get(&alice).unwrap(); assert!(direct_rooms.contains(&rooms[0])); assert!(direct_rooms.contains(&rooms[1])); } } ruma-common-0.10.5/src/events/dummy.rs000064400000000000000000000044351046102023000157430ustar 00000000000000//! Types for the [`m.dummy`] event. //! //! [`m.dummy`]: https://spec.matrix.org/v1.2/client-server-api/#mdummy use std::fmt; use ruma_macros::EventContent; use serde::{ de::{self, Deserialize, Deserializer}, ser::{Serialize, SerializeStruct as _, Serializer}, }; /// The content of an `m.dummy` event. /// /// This event is used to indicate new Olm sessions for end-to-end encryption. /// /// Typically it is encrypted as an `m.room.encrypted` event, then sent as a to-device event. /// /// The event does not have any content associated with it. The sending client is expected to /// send a key share request shortly after this message, causing the receiving client to process /// this `m.dummy` event as the most recent event and using the keyshare request to set up the /// session. The keyshare request and `m.dummy` combination should result in the original sending /// client receiving keys over the newly established session. #[derive(Clone, Debug, Default, EventContent)] #[allow(clippy::exhaustive_structs)] #[ruma_event(type = "m.dummy", kind = ToDevice)] pub struct ToDeviceDummyEventContent; impl ToDeviceDummyEventContent { /// Create a new `ToDeviceDummyEventContent`. pub fn new() -> Self { Self } } impl<'de> Deserialize<'de> for ToDeviceDummyEventContent { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = ToDeviceDummyEventContent; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a struct") } fn visit_map(self, mut map: A) -> Result where A: de::MapAccess<'de>, { while map.next_entry::()?.is_some() {} Ok(ToDeviceDummyEventContent) } } deserializer.deserialize_struct("ToDeviceDummyEventContent", &[], Visitor) } } impl Serialize for ToDeviceDummyEventContent { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_struct("ToDeviceDummyEventContent", 0)?.end() } } ruma-common-0.10.5/src/events/emote.rs000064400000000000000000000061041046102023000157140ustar 00000000000000//! Types for extensible emote message events ([MSC1767]). //! //! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ message::MessageContent, room::message::{EmoteMessageEventContent, MessageType, Relation, RoomMessageEventContent}, }; /// The payload for an extensible emote message. /// /// This is the new primary type introduced in [MSC1767] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `EmoteEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Emote`]. You can convert it back with /// [`EmoteEventContent::from_emote_room_message()`]. /// /// [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Emote`]: super::room::message::MessageType::Emote #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.emote", kind = MessageLike)] pub struct EmoteEventContent { /// The message's text content. #[serde(flatten)] pub message: MessageContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl EmoteEventContent { /// A convenience constructor to create a plain text emote. pub fn plain(body: impl Into) -> Self { Self { message: MessageContent::plain(body), relates_to: None } } /// A convenience constructor to create an HTML emote. pub fn html(body: impl Into, html_body: impl Into) -> Self { Self { message: MessageContent::html(body, html_body), relates_to: None } } /// A convenience constructor to create a Markdown emote. /// /// Returns an HTML emote if some Markdown formatting was detected, otherwise returns a plain /// text emote. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } /// Create a new `EmoteEventContent` from the given `EmoteMessageEventContent` and optional /// relation. pub fn from_emote_room_message( content: EmoteMessageEventContent, relates_to: Option, ) -> Self { let EmoteMessageEventContent { body, formatted, message, .. } = content; if let Some(message) = message { Self { message, relates_to } } else { Self { message: MessageContent::from_room_message_content(body, formatted), relates_to } } } } impl From for RoomMessageEventContent { fn from(content: EmoteEventContent) -> Self { let EmoteEventContent { message, relates_to, .. } = content; Self { msgtype: MessageType::Emote(message.into()), relates_to } } } ruma-common-0.10.5/src/events/enums.rs000064400000000000000000000363761046102023000157500ustar 00000000000000use ruma_macros::{event_enum, EventEnumFromEvent}; use serde::{de, Deserialize}; use serde_json::value::RawValue as RawJsonValue; use super::{ key, room::{encrypted, redaction::SyncRoomRedactionEvent}, Redact, Relations, }; use crate::{ serde::from_raw_json_value, EventId, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, RoomVersionId, TransactionId, UserId, }; event_enum! { /// Any global account data event. enum GlobalAccountData { "m.direct" => super::direct, "m.identity_server" => super::identity_server, "m.ignored_user_list" => super::ignored_user_list, "m.push_rules" => super::push_rules, "m.secret_storage.default_key" => super::secret_storage::default_key, "m.secret_storage.key.*" => super::secret_storage::key, } /// Any room account data event. enum RoomAccountData { "m.fully_read" => super::fully_read, "m.tag" => super::tag, } /// Any ephemeral room event. enum EphemeralRoom { "m.receipt" => super::receipt, "m.typing" => super::typing, } /// Any message-like event. enum MessageLike { #[cfg(feature = "unstable-msc3246")] "m.audio" => super::audio, "m.call.answer" => super::call::answer, "m.call.invite" => super::call::invite, "m.call.hangup" => super::call::hangup, "m.call.candidates" => super::call::candidates, #[cfg(feature = "unstable-msc2746")] "m.call.negotiate" => super::call::negotiate, #[cfg(feature = "unstable-msc2746")] "m.call.reject" => super::call::reject, #[cfg(feature = "unstable-msc2746")] "m.call.select_answer" => super::call::select_answer, #[cfg(feature = "unstable-msc1767")] "m.emote" => super::emote, #[cfg(feature = "unstable-msc3551")] "m.file" => super::file, #[cfg(feature = "unstable-msc3552")] "m.image" => super::image, "m.key.verification.ready" => super::key::verification::ready, "m.key.verification.start" => super::key::verification::start, "m.key.verification.cancel" => super::key::verification::cancel, "m.key.verification.accept" => super::key::verification::accept, "m.key.verification.key" => super::key::verification::key, "m.key.verification.mac" => super::key::verification::mac, "m.key.verification.done" => super::key::verification::done, #[cfg(feature = "unstable-msc3488")] "m.location" => super::location, #[cfg(feature = "unstable-msc1767")] "m.message" => super::message, #[cfg(feature = "unstable-msc1767")] "m.notice" => super::notice, #[cfg(feature = "unstable-msc3381")] #[ruma_enum(alias = "m.poll.start")] "org.matrix.msc3381.poll.start" => super::poll::start, #[cfg(feature = "unstable-msc3381")] #[ruma_enum(alias = "m.poll.response")] "org.matrix.msc3381.poll.response" => super::poll::response, #[cfg(feature = "unstable-msc3381")] #[ruma_enum(alias = "m.poll.end")] "org.matrix.msc3381.poll.end" => super::poll::end, #[cfg(feature = "unstable-msc2677")] "m.reaction" => super::reaction, "m.room.encrypted" => super::room::encrypted, "m.room.message" => super::room::message, "m.room.redaction" => super::room::redaction, "m.sticker" => super::sticker, #[cfg(feature = "unstable-msc3553")] "m.video" => super::video, #[cfg(feature = "unstable-msc3245")] "m.voice" => super::voice, } /// Any state event. enum State { "m.policy.rule.room" => super::policy::rule::room, "m.policy.rule.server" => super::policy::rule::server, "m.policy.rule.user" => super::policy::rule::user, "m.room.aliases" => super::room::aliases, "m.room.avatar" => super::room::avatar, "m.room.canonical_alias" => super::room::canonical_alias, "m.room.create" => super::room::create, "m.room.encryption" => super::room::encryption, "m.room.guest_access" => super::room::guest_access, "m.room.history_visibility" => super::room::history_visibility, "m.room.join_rules" => super::room::join_rules, "m.room.member" => super::room::member, "m.room.name" => super::room::name, "m.room.pinned_events" => super::room::pinned_events, "m.room.power_levels" => super::room::power_levels, "m.room.server_acl" => super::room::server_acl, "m.room.third_party_invite" => super::room::third_party_invite, "m.room.tombstone" => super::room::tombstone, "m.room.topic" => super::room::topic, "m.space.child" => super::space::child, "m.space.parent" => super::space::parent, } /// Any to-device event. enum ToDevice { "m.dummy" => super::dummy, "m.room_key" => super::room_key, "m.room_key_request" => super::room_key_request, "m.forwarded_room_key" => super::forwarded_room_key, "m.key.verification.request" => super::key::verification::request, "m.key.verification.ready" => super::key::verification::ready, "m.key.verification.start" => super::key::verification::start, "m.key.verification.cancel" => super::key::verification::cancel, "m.key.verification.accept" => super::key::verification::accept, "m.key.verification.key" => super::key::verification::key, "m.key.verification.mac" => super::key::verification::mac, "m.key.verification.done" => super::key::verification::done, "m.room.encrypted" => super::room::encrypted, "m.secret.request"=> super::secret::request, "m.secret.send" => super::secret::send, } } macro_rules! timeline_event_accessors { ( $( #[doc = $docs:literal] pub fn $field:ident(&self) -> $ty:ty; )* ) => { $( #[doc = $docs] pub fn $field(&self) -> $ty { match self { Self::MessageLike(ev) => ev.$field(), Self::State(ev) => ev.$field(), } } )* }; } /// Any room event. #[allow(clippy::large_enum_variant, clippy::exhaustive_enums)] #[derive(Clone, Debug, EventEnumFromEvent)] pub enum AnyTimelineEvent { /// Any message-like event. MessageLike(AnyMessageLikeEvent), /// Any state event. State(AnyStateEvent), } impl AnyTimelineEvent { timeline_event_accessors! { /// Returns this event's `origin_server_ts` field. pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch; /// Returns this event's `room_id` field. pub fn room_id(&self) -> &RoomId; /// Returns this event's `event_id` field. pub fn event_id(&self) -> &EventId; /// Returns this event's `sender` field. pub fn sender(&self) -> &UserId; /// Returns this event's `transaction_id` from inside `unsigned`, if there is one. pub fn transaction_id(&self) -> Option<&TransactionId>; /// Returns this event's `relations` from inside `unsigned`, if that field exists. pub fn relations(&self) -> Option<&Relations>; } } /// Any sync room event. /// /// Sync room events are room event without a `room_id`, as returned in `/sync` responses. #[allow(clippy::large_enum_variant, clippy::exhaustive_enums)] #[derive(Clone, Debug, EventEnumFromEvent)] pub enum AnySyncTimelineEvent { /// Any sync message-like event. MessageLike(AnySyncMessageLikeEvent), /// Any sync state event. State(AnySyncStateEvent), } impl AnySyncTimelineEvent { timeline_event_accessors! { /// Returns this event's `origin_server_ts` field. pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch; /// Returns this event's `event_id` field. pub fn event_id(&self) -> &EventId; /// Returns this event's `sender` field. pub fn sender(&self) -> &UserId; /// Returns this event's `transaction_id` from inside `unsigned`, if there is one. pub fn transaction_id(&self) -> Option<&TransactionId>; /// Returns this event's `relations` from inside `unsigned`, if that field exists. pub fn relations(&self) -> Option<&Relations>; } /// Converts `self` to an `AnyTimelineEvent` by adding the given a room ID. pub fn into_full_event(self, room_id: OwnedRoomId) -> AnyTimelineEvent { match self { Self::MessageLike(ev) => AnyTimelineEvent::MessageLike(ev.into_full_event(room_id)), Self::State(ev) => AnyTimelineEvent::State(ev.into_full_event(room_id)), } } } impl From for AnySyncTimelineEvent { fn from(ev: AnyTimelineEvent) -> Self { match ev { AnyTimelineEvent::MessageLike(ev) => Self::MessageLike(ev.into()), AnyTimelineEvent::State(ev) => Self::State(ev.into()), } } } #[derive(Deserialize)] #[allow(clippy::exhaustive_structs)] struct EventDeHelper { pub state_key: Option, } impl<'de> Deserialize<'de> for AnyTimelineEvent { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let EventDeHelper { state_key } = from_raw_json_value(&json)?; if state_key.is_some() { Ok(AnyTimelineEvent::State(from_raw_json_value(&json)?)) } else { Ok(AnyTimelineEvent::MessageLike(from_raw_json_value(&json)?)) } } } impl<'de> Deserialize<'de> for AnySyncTimelineEvent { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let EventDeHelper { state_key } = from_raw_json_value(&json)?; if state_key.is_some() { Ok(AnySyncTimelineEvent::State(from_raw_json_value(&json)?)) } else { Ok(AnySyncTimelineEvent::MessageLike(from_raw_json_value(&json)?)) } } } impl Redact for AnyTimelineEvent { type Redacted = Self; /// Redacts `self`, referencing the given event in `unsigned.redacted_because`. /// /// Does nothing for events that are already redacted. fn redact(self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) -> Self { match self { Self::MessageLike(ev) => Self::MessageLike(ev.redact(redaction, version)), Self::State(ev) => Self::State(ev.redact(redaction, version)), } } } impl Redact for AnySyncTimelineEvent { type Redacted = Self; /// Redacts `self`, referencing the given event in `unsigned.redacted_because`. /// /// Does nothing for events that are already redacted. fn redact(self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) -> Self { match self { Self::MessageLike(ev) => Self::MessageLike(ev.redact(redaction, version)), Self::State(ev) => Self::State(ev.redact(redaction, version)), } } } impl AnyMessageLikeEventContent { /// Get a copy of the event's `m.relates_to` field, if any. /// /// This is a helper function intended for encryption. There should not be a reason to access /// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise. pub fn relation(&self) -> Option { use super::key::verification::{ accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent, done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent, mac::KeyVerificationMacEventContent, ready::KeyVerificationReadyEventContent, start::KeyVerificationStartEventContent, }; #[cfg(feature = "unstable-msc3381")] use super::poll::{end::PollEndEventContent, response::PollResponseEventContent}; match self { #[rustfmt::skip] Self::KeyVerificationReady(KeyVerificationReadyEventContent { relates_to, .. }) | Self::KeyVerificationStart(KeyVerificationStartEventContent { relates_to, .. }) | Self::KeyVerificationCancel(KeyVerificationCancelEventContent { relates_to, .. }) | Self::KeyVerificationAccept(KeyVerificationAcceptEventContent { relates_to, .. }) | Self::KeyVerificationKey(KeyVerificationKeyEventContent { relates_to, .. }) | Self::KeyVerificationMac(KeyVerificationMacEventContent { relates_to, .. }) | Self::KeyVerificationDone(KeyVerificationDoneEventContent { relates_to, .. }) => { let key::verification::Relation { event_id } = relates_to; Some(encrypted::Relation::Reference(encrypted::Reference { event_id: event_id.clone(), })) } #[cfg(feature = "unstable-msc2677")] Self::Reaction(ev) => { use super::reaction; let reaction::Relation { event_id, key } = &ev.relates_to; Some(encrypted::Relation::Annotation(encrypted::Annotation { event_id: event_id.clone(), key: key.clone(), })) } Self::RoomEncrypted(ev) => ev.relates_to.clone(), Self::RoomMessage(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc1767")] Self::Message(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc1767")] Self::Notice(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc1767")] Self::Emote(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3245")] Self::Voice(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3246")] Self::Audio(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3488")] Self::Location(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3551")] Self::File(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3552")] Self::Image(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3553")] Self::Video(ev) => ev.relates_to.clone().map(Into::into), #[cfg(feature = "unstable-msc3381")] Self::PollResponse(PollResponseEventContent { relates_to, .. }) | Self::PollEnd(PollEndEventContent { relates_to, .. }) => { let super::poll::ReferenceRelation { event_id } = relates_to; Some(encrypted::Relation::Reference(encrypted::Reference { event_id: event_id.clone(), })) } #[cfg(feature = "unstable-msc3381")] Self::PollStart(_) => None, #[cfg(feature = "unstable-msc2746")] Self::CallNegotiate(_) | Self::CallReject(_) | Self::CallSelectAnswer(_) => None, Self::CallAnswer(_) | Self::CallInvite(_) | Self::CallHangup(_) | Self::CallCandidates(_) | Self::RoomRedaction(_) | Self::Sticker(_) | Self::_Custom { .. } => None, } } } ruma-common-0.10.5/src/events/file.rs000064400000000000000000000232551046102023000155300ustar 00000000000000//! Types for extensible file message events ([MSC3551]). //! //! [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 use std::collections::BTreeMap; use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ message::MessageContent, room::{ message::{ FileInfo, FileMessageEventContent, MessageType, Relation, RoomMessageEventContent, }, EncryptedFile, JsonWebKey, MediaSource, }, }; use crate::{serde::Base64, OwnedMxcUri}; /// The payload for an extensible file message. /// /// This is the new primary type introduced in [MSC3551] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `FileEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::File`]. You can convert it back with /// [`FileEventContent::from_file_room_message()`]. /// /// [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::File`]: super::room::message::MessageType::File #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.file", kind = MessageLike)] pub struct FileEventContent { /// The text representation of the message. #[serde(flatten)] pub message: MessageContent, /// The file content of the message. #[serde(rename = "m.file")] pub file: FileContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl FileEventContent { /// Creates a new non-encrypted `FileEventContent` with the given plain text message, url and /// file info. pub fn plain( message: impl Into, url: OwnedMxcUri, info: Option>, ) -> Self { Self { message: MessageContent::plain(message), file: FileContent::plain(url, info), relates_to: None, } } /// Creates a new non-encrypted `FileEventContent` with the given message, url and /// file info. pub fn plain_message( message: MessageContent, url: OwnedMxcUri, info: Option>, ) -> Self { Self { message, file: FileContent::plain(url, info), relates_to: None } } /// Creates a new encrypted `FileEventContent` with the given plain text message, url, /// encryption info and file info. pub fn encrypted( message: impl Into, url: OwnedMxcUri, encryption_info: EncryptedContent, info: Option>, ) -> Self { Self { message: MessageContent::plain(message), file: FileContent::encrypted(url, encryption_info, info), relates_to: None, } } /// Creates a new encrypted `FileEventContent` with the given message, url, /// encryption info and file info. pub fn encrypted_message( message: MessageContent, url: OwnedMxcUri, encryption_info: EncryptedContent, info: Option>, ) -> Self { Self { message, file: FileContent::encrypted(url, encryption_info, info), relates_to: None } } /// Create a new `FileEventContent` from the given `FileMessageEventContent` and optional /// relation. pub fn from_file_room_message( content: FileMessageEventContent, relates_to: Option, ) -> Self { let FileMessageEventContent { body, filename, source, info, message, file } = content; let FileInfo { mimetype, size, .. } = info.map(|info| *info).unwrap_or_default(); let message = message.unwrap_or_else(|| MessageContent::plain(body)); let file = file.unwrap_or_else(|| { FileContent::from_room_message_content(source, filename, mimetype, size) }); Self { message, file, relates_to } } } impl From for RoomMessageEventContent { fn from(content: FileEventContent) -> Self { let FileEventContent { message, file, relates_to } = content; Self { msgtype: MessageType::File(FileMessageEventContent::from_extensible_content( message, file, )), relates_to, } } } /// File content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct FileContent { /// The URL to the file. pub url: OwnedMxcUri, /// Information about the uploaded file. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub info: Option>, /// Information on the encrypted file. /// /// Required if the file is encrypted. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub encryption_info: Option>, } impl FileContent { /// Creates a new non-encrypted `FileContent` with the given url and file info. pub fn plain(url: OwnedMxcUri, info: Option>) -> Self { Self { url, info, encryption_info: None } } /// Creates a new encrypted `FileContent` with the given url, encryption info and file info. pub fn encrypted( url: OwnedMxcUri, encryption_info: EncryptedContent, info: Option>, ) -> Self { Self { url, info, encryption_info: Some(Box::new(encryption_info)) } } /// Create a new `FileContent` with the given media source, file info and filename. pub(crate) fn from_room_message_content( source: MediaSource, filename: Option, mimetype: Option, size: Option, ) -> Self { let (url, encryption_info) = source.into_extensible_content(); let info = FileContentInfo::from_room_message_content(filename, mimetype, size).map(Box::new); Self { url, encryption_info: encryption_info.map(Box::new), info } } /// Whether the file is encrypted. pub fn is_encrypted(&self) -> bool { self.encryption_info.is_some() } } /// Information about a file content. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct FileContentInfo { /// The original filename of the uploaded file. #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, /// The mimetype of the file, e.g. "application/msword". #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The size of the file in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, } impl FileContentInfo { /// Creates an empty `FileContentInfo`. pub fn new() -> Self { Self::default() } /// Create a new `FileContentInfo` with the given filename, mimetype and size. /// /// Returns `None` if the `FileContentInfo` would be empty. pub(crate) fn from_room_message_content( filename: Option, mimetype: Option, size: Option, ) -> Option { if filename.is_none() && mimetype.is_none() && size.is_none() { None } else { Some(Self { name: filename, mimetype, size }) } } } /// The encryption info of a file sent to a room with end-to-end encryption enabled. /// /// To create an instance of this type, first create a `EncryptedContentInit` and convert it via /// `EncryptedContent::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct EncryptedContent { /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. pub key: JsonWebKey, /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. pub iv: Base64, /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. /// /// Clients should support the SHA-256 hash, which uses the key sha256. pub hashes: BTreeMap, /// Version of the encrypted attachments protocol. /// /// Must be `v2`. pub v: String, } /// Initial set of fields of `EncryptedContent`. /// /// This struct will not be updated even if additional fields are added to `EncryptedContent` in a /// new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct EncryptedContentInit { /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. pub key: JsonWebKey, /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. pub iv: Base64, /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. /// /// Clients should support the SHA-256 hash, which uses the key sha256. pub hashes: BTreeMap, /// Version of the encrypted attachments protocol. /// /// Must be `v2`. pub v: String, } impl From for EncryptedContent { fn from(init: EncryptedContentInit) -> Self { let EncryptedContentInit { key, iv, hashes, v } = init; Self { key, iv, hashes, v } } } impl From<&EncryptedFile> for EncryptedContent { fn from(encrypted: &EncryptedFile) -> Self { let EncryptedFile { key, iv, hashes, v, .. } = encrypted; Self { key: key.to_owned(), iv: iv.to_owned(), hashes: hashes.to_owned(), v: v.to_owned() } } } ruma-common-0.10.5/src/events/forwarded_room_key.rs000064400000000000000000000077621046102023000204770ustar 00000000000000//! Types for the [`m.forwarded_room_key`] event. //! //! [`m.forwarded_room_key`]: https://spec.matrix.org/v1.2/client-server-api/#mforwarded_room_key use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{EventEncryptionAlgorithm, OwnedRoomId}; /// The content of an `m.forwarded_room_key` event. /// /// To create an instance of this type, first create a `ToDeviceForwardedRoomKeyEventContentInit` /// and convert it via `ToDeviceForwardedRoomKeyEventContent::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.forwarded_room_key", kind = ToDevice)] pub struct ToDeviceForwardedRoomKeyEventContent { /// The encryption algorithm the key in this event is to be used with. pub algorithm: EventEncryptionAlgorithm, /// The room where the key is used. pub room_id: OwnedRoomId, /// The Curve25519 key of the device which initiated the session originally. pub sender_key: String, /// The ID of the session that the key is for. pub session_id: String, /// The key to be exchanged. pub session_key: String, /// The Ed25519 key of the device which initiated the session originally. /// /// It is "claimed" because the receiving device has no way to tell that the original /// room_key actually came from a device which owns the private part of this key unless /// they have done device verification. pub sender_claimed_ed25519_key: String, /// Chain of Curve25519 keys. /// /// It starts out empty, but each time the key is forwarded to another device, the /// previous sender in the chain is added to the end of the list. For example, if the /// key is forwarded from A to B to C, this field is empty between A and B, and contains /// A's Curve25519 key between B and C. pub forwarding_curve25519_key_chain: Vec, } /// Initial set of fields of `ToDeviceForwardedRoomKeyEventContent`. /// /// This struct will not be updated even if additional fields are added to `ConditionalPushRule` in /// a new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct ToDeviceForwardedRoomKeyEventContentInit { /// The encryption algorithm the key in this event is to be used with. pub algorithm: EventEncryptionAlgorithm, /// The room where the key is used. pub room_id: OwnedRoomId, /// The Curve25519 key of the device which initiated the session originally. pub sender_key: String, /// The ID of the session that the key is for. pub session_id: String, /// The key to be exchanged. pub session_key: String, /// The Ed25519 key of the device which initiated the session originally. /// /// It is "claimed" because the receiving device has no way to tell that the original /// room_key actually came from a device which owns the private part of this key unless /// they have done device verification. pub sender_claimed_ed25519_key: String, /// Chain of Curve25519 keys. /// /// It starts out empty, but each time the key is forwarded to another device, the /// previous sender in the chain is added to the end of the list. For example, if the /// key is forwarded from A to B to C, this field is empty between A and B, and contains /// A's Curve25519 key between B and C. pub forwarding_curve25519_key_chain: Vec, } impl From for ToDeviceForwardedRoomKeyEventContent { fn from(init: ToDeviceForwardedRoomKeyEventContentInit) -> Self { Self { algorithm: init.algorithm, room_id: init.room_id, sender_key: init.sender_key, session_id: init.session_id, session_key: init.session_key, sender_claimed_ed25519_key: init.sender_claimed_ed25519_key, forwarding_curve25519_key_chain: init.forwarding_curve25519_key_chain, } } } ruma-common-0.10.5/src/events/fully_read.rs000064400000000000000000000016701046102023000167340ustar 00000000000000//! Types for the [`m.fully_read`] event. //! //! [`m.fully_read`]: https://spec.matrix.org/v1.2/client-server-api/#mfully_read use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::OwnedEventId; /// The content of an `m.fully_read` event. /// /// The current location of the user's read marker in a room. /// /// This event appears in the user's room account data for the room the marker is applicable for. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.fully_read", kind = RoomAccountData)] pub struct FullyReadEventContent { /// The event the user's read marker is located at in the room. pub event_id: OwnedEventId, } impl FullyReadEventContent { /// Creates a new `FullyReadEventContent` with the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } ruma-common-0.10.5/src/events/identity_server.rs000064400000000000000000000020311046102023000200150ustar 00000000000000//! Types for the [`m.identity_server`] event. //! //! [`m.identity_server`]: https://spec.matrix.org/v1.2/client-server-api/#mdirect use js_option::JsOption; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; /// The content of an `m.identity_server` event. /// /// Persists the user's preferred identity server, or preference to not use an identity server at /// all. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.identity_server", kind = GlobalAccountData)] pub struct IdentityServerEventContent { /// The URL of the identity server the user prefers to use, or `Null` if the user does not want /// to use an identity server. /// /// If this is `Undefined`, that means the user has not expressed a preference or has revoked /// their preference, and any applicable default should be used. #[serde(default, skip_serializing_if = "JsOption::is_undefined")] pub base_url: JsOption, } ruma-common-0.10.5/src/events/ignored_user_list.rs000064400000000000000000000044221046102023000203240ustar 00000000000000//! Types for the [`m.ignored_user_list`] event. //! //! [`m.ignored_user_list`]: https://spec.matrix.org/v1.2/client-server-api/#mignored_user_list use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::OwnedUserId; /// The content of an `m.ignored_user_list` event. /// /// A list of users to ignore. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.ignored_user_list", kind = GlobalAccountData)] pub struct IgnoredUserListEventContent { /// A list of users to ignore. #[serde(with = "crate::serde::vec_as_map_of_empty")] pub ignored_users: Vec, } impl IgnoredUserListEventContent { /// Creates a new `IgnoredUserListEventContent` from the given user IDs. pub fn new(ignored_users: Vec) -> Self { Self { ignored_users } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::IgnoredUserListEventContent; use crate::{ events::{AnyGlobalAccountDataEvent, GlobalAccountDataEvent}, user_id, }; #[test] fn serialization() { let ignored_user_list_event = GlobalAccountDataEvent { content: IgnoredUserListEventContent { ignored_users: vec![user_id!("@carl:example.com").to_owned()], }, }; let json = json!({ "content": { "ignored_users": { "@carl:example.com": {} } }, "type": "m.ignored_user_list" }); assert_eq!(to_json_value(ignored_user_list_event).unwrap(), json); } #[test] fn deserialization() { let json = json!({ "content": { "ignored_users": { "@carl:example.com": {} } }, "type": "m.ignored_user_list" }); let ev = assert_matches!( from_json_value::(json), Ok(AnyGlobalAccountDataEvent::IgnoredUserList(ev)) => ev ); assert_eq!(ev.content.ignored_users, vec![user_id!("@carl:example.com")]); } } ruma-common-0.10.5/src/events/image.rs000064400000000000000000000243161046102023000156720ustar 00000000000000//! Types for extensible image message events ([MSC3552]). //! //! [MSC3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ file::{EncryptedContent, FileContent}, message::MessageContent, room::{ message::{ImageMessageEventContent, MessageType, Relation, RoomMessageEventContent}, ImageInfo, MediaSource, ThumbnailInfo, }, }; use crate::OwnedMxcUri; /// The payload for an extensible image message. /// /// This is the new primary type introduced in [MSC3552] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `ImageEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Image`]. You can convert it back with /// [`ImageEventContent::from_image_room_message()`]. /// /// [MSC3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Image`]: super::room::message::MessageType::Image #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.image", kind = MessageLike)] pub struct ImageEventContent { /// The text representation of the message. #[serde(flatten)] pub message: MessageContent, /// The file content of the message. #[serde(rename = "m.file")] pub file: FileContent, /// The image content of the message. #[serde(rename = "m.image")] pub image: Box, /// The thumbnails of the message. #[serde(rename = "m.thumbnail", default, skip_serializing_if = "Vec::is_empty")] pub thumbnail: Vec, /// The captions of the message. #[serde( rename = "m.caption", with = "super::message::content_serde::as_vec", default, skip_serializing_if = "Option::is_none" )] pub caption: Option, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl ImageEventContent { /// Creates a new `ImageEventContent` with the given plain text message and file. pub fn plain(message: impl Into, file: FileContent) -> Self { Self { message: MessageContent::plain(message), file, image: Default::default(), thumbnail: Default::default(), caption: Default::default(), relates_to: None, } } /// Creates a new non-encrypted `ImageEventContent` with the given message and file. pub fn with_message(message: MessageContent, file: FileContent) -> Self { Self { message, file, image: Default::default(), thumbnail: Default::default(), caption: Default::default(), relates_to: None, } } /// Create a new `ImageEventContent` from the given `ImageMessageEventContent` and optional /// relation. pub fn from_image_room_message( content: ImageMessageEventContent, relates_to: Option, ) -> Self { let ImageMessageEventContent { body, source, info, message, file, image, thumbnail, caption, } = content; let ImageInfo { height, width, mimetype, size, thumbnail_info, thumbnail_source, .. } = info.map(|info| *info).unwrap_or_default(); let message = message.unwrap_or_else(|| MessageContent::plain(body)); let file = file.unwrap_or_else(|| { FileContent::from_room_message_content(source, None, mimetype, size) }); let image = image .or_else(|| ImageContent::from_room_message_content(width, height).map(Box::new)) .unwrap_or_default(); let thumbnail = thumbnail.unwrap_or_else(|| { ThumbnailContent::from_room_message_content(thumbnail_source, thumbnail_info) .into_iter() .collect() }); Self { message, file, image, thumbnail, caption, relates_to } } } impl From for RoomMessageEventContent { fn from(content: ImageEventContent) -> Self { let ImageEventContent { message, file, image, thumbnail, caption, relates_to } = content; Self { msgtype: MessageType::Image(ImageMessageEventContent::from_extensible_content( message, file, image, thumbnail, caption, )), relates_to, } } } /// Image content. #[derive(Default, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ImageContent { /// The height of the image in pixels. #[serde(skip_serializing_if = "Option::is_none")] pub height: Option, /// The width of the image in pixels. #[serde(skip_serializing_if = "Option::is_none")] pub width: Option, } impl ImageContent { /// Creates a new empty `ImageContent`. pub fn new() -> Self { Self::default() } /// Creates a new `ImageContent` with the given width and height. pub fn with_size(width: UInt, height: UInt) -> Self { Self { height: Some(height), width: Some(width) } } /// Creates a new `ImageContent` with the given optional width and height. /// /// Returns `None` if the `ImageContent` would be empty. pub(crate) fn from_room_message_content( width: Option, height: Option, ) -> Option { if width.is_none() && height.is_none() { None } else { Some(Self { width, height }) } } /// Whether this `ImageContent` is empty. pub fn is_empty(&self) -> bool { self.height.is_none() && self.width.is_none() } } /// Thumbnail content. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ThumbnailContent { /// The file info of the thumbnail. #[serde(flatten)] pub file: ThumbnailFileContent, /// The image info of the thumbnail. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub image: Option>, } impl ThumbnailContent { /// Creates a `ThumbnailContent` with the given file and image info. pub fn new(file: ThumbnailFileContent, image: Option>) -> Self { Self { file, image } } /// Create a `ThumbnailContent` with the given thumbnail source and info. /// /// Returns `None` if no thumbnail was found. pub(crate) fn from_room_message_content( source: Option, info: Option>, ) -> Option { source.map(|source| { let ThumbnailInfo { height, width, mimetype, size } = *info.unwrap_or_default(); let file = ThumbnailFileContent::from_room_message_content(source, mimetype, size); let image = ImageContent::from_room_message_content(width, height).map(Box::new); Self { file, image } }) } } /// Thumbnail file content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ThumbnailFileContent { /// The URL to the thumbnail. pub url: OwnedMxcUri, /// Information about the uploaded thumbnail. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub info: Option>, /// Information on the encrypted thumbnail. /// /// Required if the thumbnail is encrypted. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub encryption_info: Option>, } impl ThumbnailFileContent { /// Creates a new non-encrypted `ThumbnailFileContent` with the given url and file info. pub fn plain(url: OwnedMxcUri, info: Option>) -> Self { Self { url, info, encryption_info: None } } /// Creates a new encrypted `ThumbnailFileContent` with the given url, encryption info and /// thumbnail file info. pub fn encrypted( url: OwnedMxcUri, encryption_info: EncryptedContent, info: Option>, ) -> Self { Self { url, info, encryption_info: Some(Box::new(encryption_info)) } } /// Create a `ThumbnailFileContent` with the given thumbnail source and info. fn from_room_message_content( source: MediaSource, mimetype: Option, size: Option, ) -> Self { let info = ThumbnailFileContentInfo::from_room_message_content(mimetype, size).map(Box::new); match source.into_extensible_content() { (url, None) => Self::plain(url, info), (url, Some(encryption_info)) => Self::encrypted(url, encryption_info, info), } } /// Whether the thumbnail file is encrypted. pub fn is_encrypted(&self) -> bool { self.encryption_info.is_some() } } /// Information about a thumbnail file content. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ThumbnailFileContentInfo { /// The mimetype of the thumbnail, e.g. `image/png`. #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The size of the thumbnail in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, } impl ThumbnailFileContentInfo { /// Creates an empty `ThumbnailFileContentInfo`. pub fn new() -> Self { Self::default() } /// Creates a new `ThumbnailFileContentInfo` with the given optional MIME type and size. /// /// Returns `None` if the `ThumbnailFileContentInfo` would be empty. fn from_room_message_content(mimetype: Option, size: Option) -> Option { if mimetype.is_none() && size.is_none() { None } else { Some(Self { mimetype, size }) } } } ruma-common-0.10.5/src/events/key/verification/accept.rs000064400000000000000000000350241046102023000213170ustar 00000000000000//! Types for the [`m.key.verification.accept`] event. //! //! [`m.key.verification.accept`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationaccept use std::collections::BTreeMap; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use super::{ HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, Relation, ShortAuthenticationString, }; use crate::{serde::Base64, OwnedTransactionId}; /// The content of a to-device `m.key.verification.accept` event. /// /// Accepts a previously sent `m.key.verification.start` message. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.accept", kind = ToDevice)] pub struct ToDeviceKeyVerificationAcceptEventContent { /// An opaque identifier for the verification process. /// /// Must be the same as the one used for the `m.key.verification.start` message. pub transaction_id: OwnedTransactionId, /// The method specific content. #[serde(flatten)] pub method: AcceptMethod, } impl ToDeviceKeyVerificationAcceptEventContent { /// Creates a new `ToDeviceKeyVerificationAcceptEventContent` with the given transaction ID and /// method-specific content. pub fn new(transaction_id: OwnedTransactionId, method: AcceptMethod) -> Self { Self { transaction_id, method } } } /// The content of a in-room `m.key.verification.accept` event. /// /// Accepts a previously sent `m.key.verification.start` message. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[ruma_event(type = "m.key.verification.accept", kind = MessageLike)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct KeyVerificationAcceptEventContent { /// The method specific content. #[serde(flatten)] pub method: AcceptMethod, /// Information about the related event. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationAcceptEventContent { /// Creates a new `ToDeviceKeyVerificationAcceptEventContent` with the given method-specific /// content and relation. pub fn new(method: AcceptMethod, relates_to: Relation) -> Self { Self { method, relates_to } } } /// An enum representing the different method specific `m.key.verification.accept` content. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(untagged)] pub enum AcceptMethod { /// The `m.sas.v1` verification method. SasV1(SasV1Content), /// Any unknown accept method. #[doc(hidden)] _Custom(_CustomContent), } /// Method specific content of a unknown key verification method. #[doc(hidden)] #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct _CustomContent { /// The name of the method. pub method: String, /// The additional fields that the method contains. #[serde(flatten)] pub data: BTreeMap, } /// The payload of an `m.key.verification.accept` event using the `m.sas.v1` method. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(rename = "m.sas.v1", tag = "method")] pub struct SasV1Content { /// The key agreement protocol the device is choosing to use, out of the /// options in the `m.key.verification.start` message. pub key_agreement_protocol: KeyAgreementProtocol, /// The hash method the device is choosing to use, out of the options in the /// `m.key.verification.start` message. pub hash: HashAlgorithm, /// The message authentication code the device is choosing to use, out of /// the options in the `m.key.verification.start` message. pub message_authentication_code: MessageAuthenticationCode, /// The SAS methods both devices involved in the verification process /// understand. /// /// Must be a subset of the options in the `m.key.verification.start` /// message. pub short_authentication_string: Vec, /// The hash (encoded as unpadded base64) of the concatenation of the /// device's ephemeral public key (encoded as unpadded base64) and the /// canonical JSON representation of the `m.key.verification.start` message. pub commitment: Base64, } /// Mandatory initial set of fields for creating an accept `SasV1Content`. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct SasV1ContentInit { /// The key agreement protocol the device is choosing to use, out of the /// options in the `m.key.verification.start` message. pub key_agreement_protocol: KeyAgreementProtocol, /// The hash method the device is choosing to use, out of the options in the /// `m.key.verification.start` message. pub hash: HashAlgorithm, /// The message authentication codes that the accepting device understands. pub message_authentication_code: MessageAuthenticationCode, /// The SAS methods both devices involved in the verification process /// understand. /// /// Must be a subset of the options in the `m.key.verification.start` /// message. pub short_authentication_string: Vec, /// The hash (encoded as unpadded base64) of the concatenation of the /// device's ephemeral public key (encoded as unpadded base64) and the /// canonical JSON representation of the `m.key.verification.start` message. pub commitment: Base64, } impl From for SasV1Content { /// Creates a new `SasV1Content` from the given init struct. fn from(init: SasV1ContentInit) -> Self { SasV1Content { hash: init.hash, key_agreement_protocol: init.key_agreement_protocol, message_authentication_code: init.message_authentication_code, short_authentication_string: init.short_authentication_string, commitment: init.commitment, } } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, }; use super::{ AcceptMethod, HashAlgorithm, KeyAgreementProtocol, KeyVerificationAcceptEventContent, MessageAuthenticationCode, SasV1Content, ShortAuthenticationString, ToDeviceKeyVerificationAcceptEventContent, _CustomContent, }; use crate::{ event_id, events::{key::verification::Relation, ToDeviceEvent}, serde::Base64, user_id, }; #[test] fn serialization() { let key_verification_accept_content = ToDeviceKeyVerificationAcceptEventContent { transaction_id: "456".into(), method: AcceptMethod::SasV1(SasV1Content { hash: HashAlgorithm::Sha256, key_agreement_protocol: KeyAgreementProtocol::Curve25519, message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256, short_authentication_string: vec![ShortAuthenticationString::Decimal], commitment: Base64::new(b"hello".to_vec()), }), }; let sender = user_id!("@example:localhost").to_owned(); let json_data = json!({ "content": { "transaction_id": "456", "method": "m.sas.v1", "commitment": "aGVsbG8", "key_agreement_protocol": "curve25519", "hash": "sha256", "message_authentication_code": "hkdf-hmac-sha256", "short_authentication_string": ["decimal"] }, "sender": sender, "type": "m.key.verification.accept" }); let key_verification_accept = ToDeviceEvent { sender, content: key_verification_accept_content }; assert_eq!(to_json_value(&key_verification_accept).unwrap(), json_data); let sender = user_id!("@example:localhost").to_owned(); let json_data = json!({ "content": { "transaction_id": "456", "method": "m.sas.custom", "test": "field", }, "sender": sender, "type": "m.key.verification.accept" }); let key_verification_accept_content = ToDeviceKeyVerificationAcceptEventContent { transaction_id: "456".into(), method: AcceptMethod::_Custom(_CustomContent { method: "m.sas.custom".to_owned(), data: vec![("test".to_owned(), JsonValue::from("field"))] .into_iter() .collect::>(), }), }; let key_verification_accept = ToDeviceEvent { sender, content: key_verification_accept_content }; assert_eq!(to_json_value(&key_verification_accept).unwrap(), json_data); } #[test] fn in_room_serialization() { let event_id = event_id!("$1598361704261elfgc:localhost"); let key_verification_accept_content = KeyVerificationAcceptEventContent { relates_to: Relation { event_id: event_id.to_owned() }, method: AcceptMethod::SasV1(SasV1Content { hash: HashAlgorithm::Sha256, key_agreement_protocol: KeyAgreementProtocol::Curve25519, message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256, short_authentication_string: vec![ShortAuthenticationString::Decimal], commitment: Base64::new(b"hello".to_vec()), }), }; let json_data = json!({ "method": "m.sas.v1", "commitment": "aGVsbG8", "key_agreement_protocol": "curve25519", "hash": "sha256", "message_authentication_code": "hkdf-hmac-sha256", "short_authentication_string": ["decimal"], "m.relates_to": { "rel_type": "m.reference", "event_id": event_id, } }); assert_eq!(to_json_value(&key_verification_accept_content).unwrap(), json_data); } #[test] fn deserialization() { let json = json!({ "transaction_id": "456", "commitment": "aGVsbG8", "method": "m.sas.v1", "hash": "sha256", "key_agreement_protocol": "curve25519", "message_authentication_code": "hkdf-hmac-sha256", "short_authentication_string": ["decimal"] }); // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it. let content = from_json_value::(json).unwrap(); assert_eq!(content.transaction_id, "456"); let sas = assert_matches!( content.method, AcceptMethod::SasV1(sas) => sas ); assert_eq!(sas.commitment.encode(), "aGVsbG8"); assert_eq!(sas.hash, HashAlgorithm::Sha256); assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519); assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256); assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); let json = json!({ "content": { "commitment": "aGVsbG8", "transaction_id": "456", "method": "m.sas.v1", "key_agreement_protocol": "curve25519", "hash": "sha256", "message_authentication_code": "hkdf-hmac-sha256", "short_authentication_string": ["decimal"] }, "type": "m.key.verification.accept", "sender": "@example:localhost", }); let ev = from_json_value::>(json) .unwrap(); assert_eq!(ev.content.transaction_id, "456"); assert_eq!(ev.sender, "@example:localhost"); let sas = assert_matches!( ev.content.method, AcceptMethod::SasV1(sas) => sas ); assert_eq!(sas.commitment.encode(), "aGVsbG8"); assert_eq!(sas.hash, HashAlgorithm::Sha256); assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519); assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256); assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); let json = json!({ "content": { "from_device": "123", "transaction_id": "456", "method": "m.sas.custom", "test": "field", }, "type": "m.key.verification.accept", "sender": "@example:localhost", }); let ev = from_json_value::>(json) .unwrap(); assert_eq!(ev.content.transaction_id, "456"); assert_eq!(ev.sender, "@example:localhost"); let custom = assert_matches!( ev.content.method, AcceptMethod::_Custom(custom) => custom ); assert_eq!(custom.method, "m.sas.custom"); assert_eq!(custom.data.get("test"), Some(&JsonValue::from("field"))); } #[test] fn in_room_deserialization() { let json = json!({ "commitment": "aGVsbG8", "method": "m.sas.v1", "hash": "sha256", "key_agreement_protocol": "curve25519", "message_authentication_code": "hkdf-hmac-sha256", "short_authentication_string": ["decimal"], "m.relates_to": { "rel_type": "m.reference", "event_id": "$1598361704261elfgc:localhost", } }); // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it. let content = from_json_value::(json).unwrap(); assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost"); let sas = assert_matches!( content.method, AcceptMethod::SasV1(sas) => sas ); assert_eq!(sas.commitment.encode(), "aGVsbG8"); assert_eq!(sas.hash, HashAlgorithm::Sha256); assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519); assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256); assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); } } ruma-common-0.10.5/src/events/key/verification/cancel.rs000064400000000000000000000125421046102023000213050ustar 00000000000000//! Types for the [`m.key.verification.cancel`] event. //! //! [`m.key.verification.cancel`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationcancel use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::Relation; use crate::{serde::StringEnum, OwnedTransactionId, PrivOwnedStr}; /// The content of a to-device `m.key.verification.cancel` event. /// /// Cancels a key verification process/request. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.cancel", kind = ToDevice)] pub struct ToDeviceKeyVerificationCancelEventContent { /// The opaque identifier for the verification process/request. pub transaction_id: OwnedTransactionId, /// A human readable description of the `code`. /// /// The client should only rely on this string if it does not understand the `code`. pub reason: String, /// The error code for why the process / request was cancelled by the user. pub code: CancelCode, } impl ToDeviceKeyVerificationCancelEventContent { /// Creates a new `ToDeviceKeyVerificationCancelEventContent` with the given transaction ID, /// reason and code. pub fn new(transaction_id: OwnedTransactionId, reason: String, code: CancelCode) -> Self { Self { transaction_id, reason, code } } } /// The content of an in-room `m.key.verification.cancel` event. /// /// Cancels a key verification process/request. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.cancel", kind = MessageLike)] pub struct KeyVerificationCancelEventContent { /// A human readable description of the `code`. /// /// The client should only rely on this string if it does not understand the `code`. pub reason: String, /// The error code for why the process/request was cancelled by the user. pub code: CancelCode, /// Information about the related event. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationCancelEventContent { /// Creates a new `KeyVerificationCancelEventContent` with the given reason, code and relation. pub fn new(reason: String, code: CancelCode, relates_to: Relation) -> Self { Self { reason, code, relates_to } } } /// An error code for why the process/request was cancelled by the user. /// /// Custom error codes should use the Java package naming convention. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] // FIXME: Add `m.foo_bar` as a naming scheme in StringEnum and remove rename attributes. #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum CancelCode { /// The user cancelled the verification. #[ruma_enum(rename = "m.user")] User, /// The verification process timed out. /// /// Verification processes can define their own timeout parameters. #[ruma_enum(rename = "m.timeout")] Timeout, /// The device does not know about the given transaction ID. #[ruma_enum(rename = "m.unknown_transaction")] UnknownTransaction, /// The device does not know how to handle the requested method. /// /// Should be sent for `m.key.verification.start` messages and messages defined by individual /// verification processes. #[ruma_enum(rename = "m.unknown_method")] UnknownMethod, /// The device received an unexpected message. /// /// Typically raised when one of the parties is handling the verification out of order. #[ruma_enum(rename = "m.unexpected_message")] UnexpectedMessage, /// The key was not verified. #[ruma_enum(rename = "m.key_mismatch")] KeyMismatch, /// The expected user did not match the user verified. #[ruma_enum(rename = "m.user_mismatch")] UserMismatch, /// The message received was invalid. #[ruma_enum(rename = "m.invalid_message")] InvalidMessage, /// An `m.key.verification.request` was accepted by a different device. /// /// The device receiving this error can ignore the verification request. #[ruma_enum(rename = "m.accepted")] Accepted, /// The device receiving this error can ignore the verification request. #[ruma_enum(rename = "m.mismatched_commitment")] MismatchedCommitment, /// The SAS did not match. #[ruma_enum(rename = "m.mismatched_sas")] MismatchedSas, #[doc(hidden)] _Custom(PrivOwnedStr), } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::CancelCode; #[test] fn cancel_codes_serialize_to_display_form() { assert_eq!(to_json_value(&CancelCode::User).unwrap(), json!("m.user")); } #[test] fn custom_cancel_codes_serialize_to_display_form() { assert_eq!(to_json_value(CancelCode::from("io.ruma.test")).unwrap(), json!("io.ruma.test")); } #[test] fn cancel_codes_deserialize_from_display_form() { assert_eq!(from_json_value::(json!("m.user")).unwrap(), CancelCode::User); } #[test] fn custom_cancel_codes_deserialize_from_display_form() { assert_eq!( from_json_value::(json!("io.ruma.test")).unwrap(), "io.ruma.test".into() ); } } ruma-common-0.10.5/src/events/key/verification/done.rs000064400000000000000000000056501046102023000210070ustar 00000000000000//! Types for the [`m.key.verification.done`] event. //! //! [`m.key.verification.done`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationdone use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::Relation; use crate::OwnedTransactionId; /// The content of a to-device `m.m.key.verification.done` event. /// /// Event signaling that the interactive key verification has successfully concluded. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.done", kind = ToDevice)] pub struct ToDeviceKeyVerificationDoneEventContent { /// An opaque identifier for the verification process. /// /// Must be the same as the one used for the `m.key.verification.start` message. pub transaction_id: OwnedTransactionId, } impl ToDeviceKeyVerificationDoneEventContent { /// Creates a new `ToDeviceKeyVerificationDoneEventContent` with the given transaction ID. pub fn new(transaction_id: OwnedTransactionId) -> Self { Self { transaction_id } } } /// The payload for a in-room `m.key.verification.done` event. /// /// Event signaling that the interactive key verification has successfully concluded. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.done", kind = MessageLike)] pub struct KeyVerificationDoneEventContent { /// Relation signaling which verification request this event is responding to. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationDoneEventContent { /// Creates a new `KeyVerificationDoneEventContent` with the given relation. pub fn new(relates_to: Relation) -> Self { Self { relates_to } } } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::KeyVerificationDoneEventContent; use crate::{event_id, events::key::verification::Relation}; #[test] fn serialization() { let event_id = event_id!("$1598361704261elfgc:localhost").to_owned(); let json_data = json!({ "m.relates_to": { "rel_type": "m.reference", "event_id": event_id, } }); let content = KeyVerificationDoneEventContent { relates_to: Relation { event_id } }; assert_eq!(to_json_value(&content).unwrap(), json_data); } #[test] fn deserialization() { let json_data = json!({ "m.relates_to": { "rel_type": "m.reference", "event_id": "$1598361704261elfgc:localhost", } }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost"); } } ruma-common-0.10.5/src/events/key/verification/key.rs000064400000000000000000000040261046102023000206460ustar 00000000000000//! Types for the [`m.key.verification.key`] event. //! //! [`m.key.verification.key`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationkey use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::Relation; use crate::{serde::Base64, OwnedTransactionId}; /// The content of a to-device `m.key.verification.key` event. /// /// Sends the ephemeral public key for a device to the partner device. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.key", kind = ToDevice)] pub struct ToDeviceKeyVerificationKeyEventContent { /// An opaque identifier for the verification process. /// /// Must be the same as the one used for the `m.key.verification.start` message. pub transaction_id: OwnedTransactionId, /// The device's ephemeral public key, encoded as unpadded base64. pub key: Base64, } impl ToDeviceKeyVerificationKeyEventContent { /// Creates a new `ToDeviceKeyVerificationKeyEventContent` with the given transaction ID and /// key. pub fn new(transaction_id: OwnedTransactionId, key: Base64) -> Self { Self { transaction_id, key } } } /// The content of an in-room `m.key.verification.key` event. /// /// Sends the ephemeral public key for a device to the partner device. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.key", kind = MessageLike)] pub struct KeyVerificationKeyEventContent { /// The device's ephemeral public key, encoded as unpadded base64. pub key: Base64, /// Information about the related event. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationKeyEventContent { /// Creates a new `KeyVerificationKeyEventContent` with the given key and relation. pub fn new(key: Base64, relates_to: Relation) -> Self { Self { key, relates_to } } } ruma-common-0.10.5/src/events/key/verification/mac.rs000064400000000000000000000052661046102023000206250ustar 00000000000000//! Types for the [`m.key.verification.mac`] event. //! //! [`m.key.verification.mac`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationmac use std::collections::BTreeMap; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::Relation; use crate::{serde::Base64, OwnedTransactionId}; /// The content of a to-device `m.key.verification.` event. /// /// Sends the MAC of a device's key to the partner device. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.mac", kind = ToDevice)] pub struct ToDeviceKeyVerificationMacEventContent { /// An opaque identifier for the verification process. /// /// Must be the same as the one used for the `m.key.verification.start` message. pub transaction_id: OwnedTransactionId, /// A map of the key ID to the MAC of the key, using the algorithm in the verification process. /// /// The MAC is encoded as unpadded base64. pub mac: BTreeMap, /// The MAC of the comma-separated, sorted, list of key IDs given in the `mac` property, /// encoded as unpadded base64. pub keys: Base64, } impl ToDeviceKeyVerificationMacEventContent { /// Creates a new `ToDeviceKeyVerificationMacEventContent` with the given transaction ID, key ID /// to MAC map and key MAC. pub fn new( transaction_id: OwnedTransactionId, mac: BTreeMap, keys: Base64, ) -> Self { Self { transaction_id, mac, keys } } } /// The content of an in-room `m.key.verification.` event. /// /// Sends the MAC of a device's key to the partner device. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.mac", kind = MessageLike)] pub struct KeyVerificationMacEventContent { /// A map of the key ID to the MAC of the key, using the algorithm in the verification process. /// /// The MAC is encoded as unpadded base64. pub mac: BTreeMap, /// The MAC of the comma-separated, sorted, list of key IDs given in the `mac` property, /// encoded as unpadded base64. pub keys: Base64, /// Information about the related event. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationMacEventContent { /// Creates a new `KeyVerificationMacEventContent` with the given key ID to MAC map, key MAC and /// relation. pub fn new(mac: BTreeMap, keys: Base64, relates_to: Relation) -> Self { Self { mac, keys, relates_to } } } ruma-common-0.10.5/src/events/key/verification/ready.rs000064400000000000000000000121261046102023000211620ustar 00000000000000//! Types for the [`m.key.verification.ready`] event. //! //! [`m.key.verification.ready`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationready use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{Relation, VerificationMethod}; use crate::{OwnedDeviceId, OwnedTransactionId}; /// The content of a to-device `m.m.key.verification.ready` event. /// /// Response to a previously sent `m.key.verification.request` message. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.ready", kind = ToDevice)] pub struct ToDeviceKeyVerificationReadyEventContent { /// The device ID which is initiating the request. pub from_device: OwnedDeviceId, /// The verification methods supported by the sender. pub methods: Vec, /// An opaque identifier for the verification process. /// /// Must be unique with respect to the devices involved. Must be the same as the /// `transaction_id` given in the `m.key.verification.request` from a /// request. pub transaction_id: OwnedTransactionId, } impl ToDeviceKeyVerificationReadyEventContent { /// Creates a new `ToDeviceKeyVerificationReadyEventContent` with the given device ID, /// verification methods and transaction ID. pub fn new( from_device: OwnedDeviceId, methods: Vec, transaction_id: OwnedTransactionId, ) -> Self { Self { from_device, methods, transaction_id } } } /// The content of an in-room `m.m.key.verification.ready` event. /// /// Response to a previously sent `m.key.verification.request` message. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.ready", kind = MessageLike)] pub struct KeyVerificationReadyEventContent { /// The device ID which is initiating the request. pub from_device: OwnedDeviceId, /// The verification methods supported by the sender. pub methods: Vec, /// Relation signaling which verification request this event is responding /// to. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationReadyEventContent { /// Creates a new `KeyVerificationReadyEventContent` with the given device ID, methods and /// relation. pub fn new( from_device: OwnedDeviceId, methods: Vec, relates_to: Relation, ) -> Self { Self { from_device, methods, relates_to } } } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{KeyVerificationReadyEventContent, ToDeviceKeyVerificationReadyEventContent}; use crate::{ event_id, events::key::verification::{Relation, VerificationMethod}, OwnedDeviceId, }; #[test] fn serialization() { let event_id = event_id!("$1598361704261elfgc:localhost").to_owned(); let device: OwnedDeviceId = "123".into(); let json_data = json!({ "from_device": device, "methods": ["m.sas.v1"], "m.relates_to": { "rel_type": "m.reference", "event_id": event_id, } }); let content = KeyVerificationReadyEventContent { from_device: device.clone(), relates_to: Relation { event_id }, methods: vec![VerificationMethod::SasV1], }; assert_eq!(to_json_value(&content).unwrap(), json_data); let json_data = json!({ "from_device": device, "methods": ["m.sas.v1"], "transaction_id": "456", }); let content = ToDeviceKeyVerificationReadyEventContent { from_device: device, transaction_id: "456".into(), methods: vec![VerificationMethod::SasV1], }; assert_eq!(to_json_value(&content).unwrap(), json_data); } #[test] fn deserialization() { let json_data = json!({ "from_device": "123", "methods": ["m.sas.v1"], "m.relates_to": { "rel_type": "m.reference", "event_id": "$1598361704261elfgc:localhost", } }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.from_device, "123"); assert_eq!(content.methods, vec![VerificationMethod::SasV1]); assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost"); let json_data = json!({ "from_device": "123", "methods": ["m.sas.v1"], "transaction_id": "456", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.from_device, "123"); assert_eq!(content.methods, vec![VerificationMethod::SasV1]); assert_eq!(content.transaction_id, "456"); } } ruma-common-0.10.5/src/events/key/verification/request.rs000064400000000000000000000033511046102023000215460ustar 00000000000000//! Types for the [`m.key.verification.request`] event. //! //! [`m.key.verification.request`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationrequest use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::VerificationMethod; use crate::{MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedTransactionId}; /// The content of an `m.key.verification.request` event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.request", kind = ToDevice)] pub struct ToDeviceKeyVerificationRequestEventContent { /// The device ID which is initiating the request. pub from_device: OwnedDeviceId, /// An opaque identifier for the verification request. /// /// Must be unique with respect to the devices involved. pub transaction_id: OwnedTransactionId, /// The verification methods supported by the sender. pub methods: Vec, /// The time in milliseconds for when the request was made. /// /// If the request is in the future by more than 5 minutes or more than 10 minutes in /// the past, the message should be ignored by the receiver. pub timestamp: MilliSecondsSinceUnixEpoch, } impl ToDeviceKeyVerificationRequestEventContent { /// Creates a new `ToDeviceKeyVerificationRequestEventContent` with the given device ID, /// transaction ID, methods and timestamp. pub fn new( from_device: OwnedDeviceId, transaction_id: OwnedTransactionId, methods: Vec, timestamp: MilliSecondsSinceUnixEpoch, ) -> Self { Self { from_device, transaction_id, methods, timestamp } } } ruma-common-0.10.5/src/events/key/verification/start.rs000064400000000000000000000456161046102023000212250ustar 00000000000000//! Types for the [`m.key.verification.start`] event. //! //! [`m.key.verification.start`]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationstart use std::collections::BTreeMap; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use super::{ HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, Relation, ShortAuthenticationString, }; use crate::{serde::Base64, OwnedDeviceId, OwnedTransactionId}; /// The content of a to-device `m.key.verification.start` event. /// /// Begins an SAS key verification process. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.start", kind = ToDevice)] pub struct ToDeviceKeyVerificationStartEventContent { /// The device ID which is initiating the process. pub from_device: OwnedDeviceId, /// An opaque identifier for the verification process. /// /// Must be unique with respect to the devices involved. Must be the same as the /// `transaction_id` given in the `m.key.verification.request` if this process is originating /// from a request. pub transaction_id: OwnedTransactionId, /// Method specific content. #[serde(flatten)] pub method: StartMethod, } impl ToDeviceKeyVerificationStartEventContent { /// Creates a new `ToDeviceKeyVerificationStartEventContent` with the given device ID, /// transaction ID and method specific content. pub fn new( from_device: OwnedDeviceId, transaction_id: OwnedTransactionId, method: StartMethod, ) -> Self { Self { from_device, transaction_id, method } } } /// The content of an in-room `m.key.verification.start` event. /// /// Begins an SAS key verification process. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.key.verification.start", kind = MessageLike)] pub struct KeyVerificationStartEventContent { /// The device ID which is initiating the process. pub from_device: OwnedDeviceId, /// Method specific content. #[serde(flatten)] pub method: StartMethod, /// Information about the related event. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl KeyVerificationStartEventContent { /// Creates a new `KeyVerificationStartEventContent` with the given device ID, method and /// relation. pub fn new(from_device: OwnedDeviceId, method: StartMethod, relates_to: Relation) -> Self { Self { from_device, method, relates_to } } } /// An enum representing the different method specific `m.key.verification.start` content. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(untagged)] pub enum StartMethod { /// The `m.sas.v1` verification method. SasV1(SasV1Content), /// The `m.reciprocate.v1` verification method. /// /// The spec entry for this method can be found [here]. /// /// [here]: https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationstartmreciprocatev1 ReciprocateV1(ReciprocateV1Content), /// Any unknown start method. #[doc(hidden)] _Custom(_CustomContent), } /// Method specific content of a unknown key verification method. #[doc(hidden)] #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct _CustomContent { /// The name of the method. pub method: String, /// The additional fields that the method contains. #[serde(flatten)] pub data: BTreeMap, } /// The payload of an `m.key.verification.start` event using the `m.sas.v1` method. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(rename = "m.reciprocate.v1", tag = "method")] pub struct ReciprocateV1Content { /// The shared secret from the QR code, encoded using unpadded base64. pub secret: Base64, } impl ReciprocateV1Content { /// Create a new `ReciprocateV1Content` with the given shared secret. /// /// The shared secret needs to come from the scanned QR code, encoded using unpadded base64. pub fn new(secret: Base64) -> Self { Self { secret } } } /// The payload of an `m.key.verification.start` event using the `m.sas.v1` method. /// /// To create an instance of this type, first create a `SasV1ContentInit` and convert it via /// `SasV1Content::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(rename = "m.sas.v1", tag = "method")] pub struct SasV1Content { /// The key agreement protocols the sending device understands. /// /// Must include at least `Curve25519` or `Curve25519HkdfSha256`. pub key_agreement_protocols: Vec, /// The hash methods the sending device understands. /// /// Must include at least `sha256`. pub hashes: Vec, /// The message authentication codes that the sending device understands. /// /// Must include at least `hkdf-hmac-sha256`. pub message_authentication_codes: Vec, /// The SAS methods the sending device (and the sending device's user) understands. /// /// Must include at least `decimal`. Optionally can include `emoji`. pub short_authentication_string: Vec, } /// Mandatory initial set of fields for creating an `SasV1Content`. /// /// This struct will not be updated even if additional fields are added to `SasV1Content` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct SasV1ContentInit { /// The key agreement protocols the sending device understands. /// /// Should include at least `curve25519`. pub key_agreement_protocols: Vec, /// The hash methods the sending device understands. /// /// Should include at least `sha256`. pub hashes: Vec, /// The message authentication codes that the sending device understands. /// /// Should include at least `hkdf-hmac-sha256`. pub message_authentication_codes: Vec, /// The SAS methods the sending device (and the sending device's user) understands. /// /// Should include at least `decimal`. pub short_authentication_string: Vec, } impl From for SasV1Content { /// Creates a new `SasV1Content` from the given init struct. fn from(init: SasV1ContentInit) -> Self { Self { key_agreement_protocols: init.key_agreement_protocols, hashes: init.hashes, message_authentication_codes: init.message_authentication_codes, short_authentication_string: init.short_authentication_string, } } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, }; use super::{ HashAlgorithm, KeyAgreementProtocol, KeyVerificationStartEventContent, MessageAuthenticationCode, ReciprocateV1Content, SasV1ContentInit, ShortAuthenticationString, StartMethod, ToDeviceKeyVerificationStartEventContent, _CustomContent, }; use crate::{ event_id, events::{key::verification::Relation, ToDeviceEvent}, serde::Base64, user_id, }; #[test] fn serialization() { let key_verification_start_content = ToDeviceKeyVerificationStartEventContent { from_device: "123".into(), transaction_id: "456".into(), method: StartMethod::SasV1( SasV1ContentInit { hashes: vec![HashAlgorithm::Sha256], key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519], message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256], short_authentication_string: vec![ShortAuthenticationString::Decimal], } .into(), ), }; let sender = user_id!("@example:localhost").to_owned(); let json_data = json!({ "content": { "from_device": "123", "transaction_id": "456", "method": "m.sas.v1", "key_agreement_protocols": ["curve25519"], "hashes": ["sha256"], "message_authentication_codes": ["hkdf-hmac-sha256"], "short_authentication_string": ["decimal"] }, "type": "m.key.verification.start", "sender": sender }); let key_verification_start = ToDeviceEvent { sender, content: key_verification_start_content }; assert_eq!(to_json_value(&key_verification_start).unwrap(), json_data); let sender = user_id!("@example:localhost").to_owned(); let json_data = json!({ "content": { "from_device": "123", "transaction_id": "456", "method": "m.sas.custom", "test": "field", }, "type": "m.key.verification.start", "sender": sender }); let key_verification_start_content = ToDeviceKeyVerificationStartEventContent { from_device: "123".into(), transaction_id: "456".into(), method: StartMethod::_Custom(_CustomContent { method: "m.sas.custom".to_owned(), data: vec![("test".to_owned(), JsonValue::from("field"))] .into_iter() .collect::>(), }), }; let key_verification_start = ToDeviceEvent { sender, content: key_verification_start_content }; assert_eq!(to_json_value(&key_verification_start).unwrap(), json_data); { let secret = Base64::new(b"This is a secret to everybody".to_vec()); let key_verification_start_content = ToDeviceKeyVerificationStartEventContent { from_device: "123".into(), transaction_id: "456".into(), method: StartMethod::ReciprocateV1(ReciprocateV1Content::new(secret.clone())), }; let json_data = json!({ "from_device": "123", "method": "m.reciprocate.v1", "secret": secret, "transaction_id": "456" }); assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data); } } #[test] fn in_room_serialization() { let event_id = event_id!("$1598361704261elfgc:localhost"); let key_verification_start_content = KeyVerificationStartEventContent { from_device: "123".into(), relates_to: Relation { event_id: event_id.to_owned() }, method: StartMethod::SasV1( SasV1ContentInit { hashes: vec![HashAlgorithm::Sha256], key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519], message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256], short_authentication_string: vec![ShortAuthenticationString::Decimal], } .into(), ), }; let json_data = json!({ "from_device": "123", "method": "m.sas.v1", "key_agreement_protocols": ["curve25519"], "hashes": ["sha256"], "message_authentication_codes": ["hkdf-hmac-sha256"], "short_authentication_string": ["decimal"], "m.relates_to": { "rel_type": "m.reference", "event_id": event_id, } }); assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data); let secret = Base64::new(b"This is a secret to everybody".to_vec()); let key_verification_start_content = KeyVerificationStartEventContent { from_device: "123".into(), relates_to: Relation { event_id: event_id.to_owned() }, method: StartMethod::ReciprocateV1(ReciprocateV1Content::new(secret.clone())), }; let json_data = json!({ "from_device": "123", "method": "m.reciprocate.v1", "secret": secret, "m.relates_to": { "rel_type": "m.reference", "event_id": event_id, } }); assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data); } #[test] fn deserialization() { let json = json!({ "from_device": "123", "transaction_id": "456", "method": "m.sas.v1", "hashes": ["sha256"], "key_agreement_protocols": ["curve25519"], "message_authentication_codes": ["hkdf-hmac-sha256"], "short_authentication_string": ["decimal"] }); // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it. let content = from_json_value::(json).unwrap(); assert_eq!(content.from_device, "123"); assert_eq!(content.transaction_id, "456"); let sas = assert_matches!( content.method, StartMethod::SasV1(sas) => sas ); assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]); assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]); assert_eq!( sas.message_authentication_codes, vec![MessageAuthenticationCode::HkdfHmacSha256] ); assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); let json = json!({ "content": { "from_device": "123", "transaction_id": "456", "method": "m.sas.v1", "key_agreement_protocols": ["curve25519"], "hashes": ["sha256"], "message_authentication_codes": ["hkdf-hmac-sha256"], "short_authentication_string": ["decimal"] }, "type": "m.key.verification.start", "sender": "@example:localhost", }); let ev = from_json_value::>(json) .unwrap(); assert_eq!(ev.sender, "@example:localhost"); assert_eq!(ev.content.from_device, "123"); assert_eq!(ev.content.transaction_id, "456"); let sas = assert_matches!( ev.content.method, StartMethod::SasV1(sas) => sas ); assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]); assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]); assert_eq!( sas.message_authentication_codes, vec![MessageAuthenticationCode::HkdfHmacSha256] ); assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); let json = json!({ "content": { "from_device": "123", "transaction_id": "456", "method": "m.sas.custom", "test": "field", }, "type": "m.key.verification.start", "sender": "@example:localhost", }); let ev = from_json_value::>(json) .unwrap(); assert_eq!(ev.sender, "@example:localhost"); assert_eq!(ev.content.from_device, "123"); assert_eq!(ev.content.transaction_id, "456"); let custom = assert_matches!( ev.content.method, StartMethod::_Custom(custom) => custom ); assert_eq!(custom.method, "m.sas.custom"); assert_eq!(custom.data.get("test"), Some(&JsonValue::from("field"))); let json = json!({ "content": { "from_device": "123", "method": "m.reciprocate.v1", "secret": "c2VjcmV0Cg", "transaction_id": "456", }, "type": "m.key.verification.start", "sender": "@example:localhost", }); let ev = from_json_value::>(json) .unwrap(); assert_eq!(ev.sender, "@example:localhost"); assert_eq!(ev.content.from_device, "123"); assert_eq!(ev.content.transaction_id, "456"); let reciprocate = assert_matches!( ev.content.method, StartMethod::ReciprocateV1(reciprocate) => reciprocate ); assert_eq!(reciprocate.secret.encode(), "c2VjcmV0Cg"); } #[test] fn in_room_deserialization() { let json = json!({ "from_device": "123", "method": "m.sas.v1", "hashes": ["sha256"], "key_agreement_protocols": ["curve25519"], "message_authentication_codes": ["hkdf-hmac-sha256"], "short_authentication_string": ["decimal"], "m.relates_to": { "rel_type": "m.reference", "event_id": "$1598361704261elfgc:localhost", } }); // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it. let content = from_json_value::(json).unwrap(); assert_eq!(content.from_device, "123"); assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost"); let sas = assert_matches!( content.method, StartMethod::SasV1(sas) => sas ); assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]); assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]); assert_eq!( sas.message_authentication_codes, vec![MessageAuthenticationCode::HkdfHmacSha256] ); assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]); let json = json!({ "from_device": "123", "method": "m.reciprocate.v1", "secret": "c2VjcmV0Cg", "m.relates_to": { "rel_type": "m.reference", "event_id": "$1598361704261elfgc:localhost", } }); let content = from_json_value::(json).unwrap(); assert_eq!(content.from_device, "123"); assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost"); let reciprocate = assert_matches!( content.method, StartMethod::ReciprocateV1(reciprocate) => reciprocate ); assert_eq!(reciprocate.secret.encode(), "c2VjcmV0Cg"); } } ruma-common-0.10.5/src/events/key/verification.rs000064400000000000000000000117161046102023000200620ustar 00000000000000//! Modules for events in the `m.key.verification` namespace. //! //! This module also contains types shared by events in its child namespaces. //! //! The MSC for the in-room variants of the `m.key.verification.*` events can be found on //! [MSC2241]. //! //! [MSC2241]: https://github.com/matrix-org/matrix-spec-proposals/pull/2241 use serde::{Deserialize, Serialize}; use crate::{serde::StringEnum, OwnedEventId, PrivOwnedStr}; pub mod accept; pub mod cancel; pub mod done; pub mod key; pub mod mac; pub mod ready; pub mod request; pub mod start; /// A hash algorithm. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum HashAlgorithm { /// The SHA256 hash algorithm. Sha256, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A key agreement protocol. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "kebab-case")] #[non_exhaustive] pub enum KeyAgreementProtocol { /// The [Curve25519](https://cr.yp.to/ecdh.html) key agreement protocol. Curve25519, /// The Curve25519 key agreement protocol with check for public keys. Curve25519HkdfSha256, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A message authentication code algorithm. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "kebab-case")] #[non_exhaustive] pub enum MessageAuthenticationCode { /// The HKDF-HMAC-SHA256 MAC. HkdfHmacSha256, /// The HMAC-SHA256 MAC. HmacSha256, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A Short Authentication String method. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum ShortAuthenticationString { /// The decimal method. Decimal, /// The emoji method. Emoji, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A relation which associates an `m.key.verification.request` with another key verification event. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "rel_type", rename = "m.reference")] pub struct Relation { /// The event ID of a related `m.key.verification.request`. pub event_id: OwnedEventId, } impl Relation { /// Creates a new `Relation` with the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } /// A Short Authentication String (SAS) verification method. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum VerificationMethod { /// The `m.sas.v1` verification method. #[ruma_enum(rename = "m.sas.v1")] SasV1, /// The `m.qr_code.scan.v1` verification method. #[ruma_enum(rename = "m.qr_code.scan.v1")] QrCodeScanV1, /// The `m.qr_code.show.v1` verification method. #[ruma_enum(rename = "m.qr_code.show.v1")] QrCodeShowV1, /// The `m.reciprocate.v1` verification method. #[ruma_enum(rename = "m.reciprocate.v1")] ReciprocateV1, #[doc(hidden)] _Custom(PrivOwnedStr), } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json}; use super::{KeyAgreementProtocol, MessageAuthenticationCode}; #[test] fn serialize_key_agreement() { let serialized = serde_json::to_string(&KeyAgreementProtocol::Curve25519HkdfSha256).unwrap(); assert_eq!(serialized, "\"curve25519-hkdf-sha256\""); let deserialized: KeyAgreementProtocol = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, KeyAgreementProtocol::Curve25519HkdfSha256); } #[test] fn deserialize_mac_method() { let json = json!(["hkdf-hmac-sha256", "hmac-sha256"]); let deserialized: Vec = from_json_value(json).unwrap(); assert!(deserialized.contains(&MessageAuthenticationCode::HkdfHmacSha256)); } #[test] fn serialize_mac_method() { let serialized = serde_json::to_string(&MessageAuthenticationCode::HkdfHmacSha256).unwrap(); let deserialized: MessageAuthenticationCode = serde_json::from_str(&serialized).unwrap(); assert_eq!(serialized, "\"hkdf-hmac-sha256\""); assert_eq!(deserialized, MessageAuthenticationCode::HkdfHmacSha256); let serialized = serde_json::to_string(&MessageAuthenticationCode::HmacSha256).unwrap(); let deserialized: MessageAuthenticationCode = serde_json::from_str(&serialized).unwrap(); assert_eq!(serialized, "\"hmac-sha256\""); assert_eq!(deserialized, MessageAuthenticationCode::HmacSha256); } } ruma-common-0.10.5/src/events/key.rs000064400000000000000000000001101046102023000153620ustar 00000000000000//! Modules for events in the `m.key` namespace. pub mod verification; ruma-common-0.10.5/src/events/kinds.rs000064400000000000000000000532621046102023000157220ustar 00000000000000#![allow(clippy::exhaustive_structs)] use ruma_macros::Event; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::value::RawValue as RawJsonValue; use super::{ room::redaction::SyncRoomRedactionEvent, EphemeralRoomEventContent, EventContent, GlobalAccountDataEventContent, MessageLikeEventContent, MessageLikeEventType, MessageLikeUnsigned, Redact, RedactContent, RedactedMessageLikeEventContent, RedactedStateEventContent, RedactedUnsigned, RedactionDeHelper, RoomAccountDataEventContent, StateEventContent, StateEventType, ToDeviceEventContent, }; use crate::{ serde::from_raw_json_value, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, UserId, }; /// A global account data event. #[derive(Clone, Debug, Event)] pub struct GlobalAccountDataEvent { /// Data specific to the event type. pub content: C, } /// A room account data event. #[derive(Clone, Debug, Event)] pub struct RoomAccountDataEvent { /// Data specific to the event type. pub content: C, } /// An ephemeral room event. #[derive(Clone, Debug, Event)] pub struct EphemeralRoomEvent { /// Data specific to the event type. pub content: C, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, } /// An ephemeral room event without a `room_id`. #[derive(Clone, Debug, Event)] pub struct SyncEphemeralRoomEvent { /// Data specific to the event type. pub content: C, } /// An unredacted message-like event. /// /// `OriginalMessageLikeEvent` implements the comparison traits using only the `event_id` field, a /// sorted list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct OriginalMessageLikeEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, /// Additional key-value pairs not signed by the homeserver. pub unsigned: MessageLikeUnsigned, } /// An unredacted message-like event without a `room_id`. /// /// `OriginalSyncMessageLikeEvent` implements the comparison traits using only the `event_id` field, /// a sorted list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct OriginalSyncMessageLikeEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// Additional key-value pairs not signed by the homeserver. pub unsigned: MessageLikeUnsigned, } /// A redacted message-like event. /// /// `RedactedMessageLikeEvent` implements the comparison traits using only the `event_id` field, a /// sorted list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct RedactedMessageLikeEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, /// Additional key-value pairs not signed by the homeserver. pub unsigned: RedactedUnsigned, } /// A redacted message-like event without a `room_id`. /// /// `RedactedSyncMessageLikeEvent` implements the comparison traits using only the `event_id` field, /// a sorted list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct RedactedSyncMessageLikeEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// Additional key-value pairs not signed by the homeserver. pub unsigned: RedactedUnsigned, } /// A possibly-redacted message-like event. /// /// `MessageLikeEvent` implements the comparison traits using only the `event_id` field, a sorted /// list would be sorted lexicographically based on the event's `EventId`. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum MessageLikeEvent where C::Redacted: RedactedMessageLikeEventContent, { /// Original, unredacted form of the event. Original(OriginalMessageLikeEvent), /// Redacted form of the event with minimal fields. Redacted(RedactedMessageLikeEvent), } /// A possibly-redacted message-like event without a `room_id`. /// /// `SyncMessageLikeEvent` implements the comparison traits using only the `event_id` field, a /// sorted list would be sorted lexicographically based on the event's `EventId`. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum SyncMessageLikeEvent where C::Redacted: RedactedMessageLikeEventContent, { /// Original, unredacted form of the event. Original(OriginalSyncMessageLikeEvent), /// Redacted form of the event with minimal fields. Redacted(RedactedSyncMessageLikeEvent), } /// An unredacted state event. /// /// `OriginalStateEvent` implements the comparison traits using only the `event_id` field, a sorted /// list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct OriginalStateEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, /// A unique key which defines the overwriting semantics for this piece of room state. /// /// This is often an empty string, but some events send a `UserId` to show which user the event /// affects. pub state_key: C::StateKey, /// Additional key-value pairs not signed by the homeserver. pub unsigned: C::Unsigned, } /// An unredacted state event without a `room_id`. /// /// `OriginalSyncStateEvent` implements the comparison traits using only the `event_id` field, a /// sorted list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct OriginalSyncStateEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// A unique key which defines the overwriting semantics for this piece of room state. /// /// This is often an empty string, but some events send a `UserId` to show which user the event /// affects. pub state_key: C::StateKey, /// Additional key-value pairs not signed by the homeserver. pub unsigned: C::Unsigned, } /// A stripped-down state event, used for previews of rooms the user has been invited to. #[derive(Clone, Debug, Event)] pub struct StrippedStateEvent { /// Data specific to the event type. pub content: C, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// A unique key which defines the overwriting semantics for this piece of room state. /// /// This is often an empty string, but some events send a `UserId` to show which user the event /// affects. pub state_key: C::StateKey, } /// A minimal state event, used for creating a new room. #[derive(Clone, Debug, Event)] pub struct InitialStateEvent { /// Data specific to the event type. pub content: C, /// A unique key which defines the overwriting semantics for this piece of room state. /// /// This is often an empty string, but some events send a `UserId` to show which user the event /// affects. /// /// Defaults to the empty string. pub state_key: C::StateKey, } /// A redacted state event. /// /// `RedactedStateEvent` implements the comparison traits using only the `event_id` field, a sorted /// list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct RedactedStateEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, /// A unique key which defines the overwriting semantics for this piece of room state. /// /// This is often an empty string, but some events send a `UserId` to show which user the event /// affects. pub state_key: C::StateKey, /// Additional key-value pairs not signed by the homeserver. pub unsigned: RedactedUnsigned, } /// A redacted state event without a `room_id`. /// /// `RedactedSyncStateEvent` implements the comparison traits using only the `event_id` field, a /// sorted list would be sorted lexicographically based on the event's `EventId`. #[derive(Clone, Debug, Event)] pub struct RedactedSyncStateEvent { /// Data specific to the event type. pub content: C, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// A unique key which defines the overwriting semantics for this piece of room state. /// /// This is often an empty string, but some events send a `UserId` to show which user the event /// affects. pub state_key: C::StateKey, /// Additional key-value pairs not signed by the homeserver. pub unsigned: RedactedUnsigned, } /// A possibly-redacted state event. /// /// `StateEvent` implements the comparison traits using only the `event_id` field, a sorted list /// would be sorted lexicographically based on the event's `EventId`. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum StateEvent where C::Redacted: RedactedStateEventContent, { /// Original, unredacted form of the event. Original(OriginalStateEvent), /// Redacted form of the event with minimal fields. Redacted(RedactedStateEvent), } /// A possibly-redacted state event without a `room_id`. /// /// `SyncStateEvent` implements the comparison traits using only the `event_id` field, a sorted list /// would be sorted lexicographically based on the event's `EventId`. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum SyncStateEvent where C::Redacted: RedactedStateEventContent, { /// Original, unredacted form of the event. Original(OriginalSyncStateEvent), /// Redacted form of the event with minimal fields. Redacted(RedactedSyncStateEvent), } /// An event sent using send-to-device messaging. #[derive(Clone, Debug, Event)] pub struct ToDeviceEvent { /// Data specific to the event type. pub content: C, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, } /// The decrypted payload of an `m.olm.v1.curve25519-aes-sha2` event. #[derive(Clone, Debug, Event)] pub struct DecryptedOlmV1Event { /// Data specific to the event type. pub content: C, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// The fully-qualified ID of the intended recipient this event. pub recipient: OwnedUserId, /// The recipient's ed25519 key. pub recipient_keys: OlmV1Keys, /// The sender's ed25519 key. pub keys: OlmV1Keys, } /// Public keys used for an `m.olm.v1.curve25519-aes-sha2` event. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct OlmV1Keys { /// An ed25519 key. pub ed25519: String, } /// The decrypted payload of an `m.megolm.v1.aes-sha2` event. #[derive(Clone, Debug, Event)] pub struct DecryptedMegolmV1Event { /// Data specific to the event type. pub content: C, /// The ID of the room associated with the event. pub room_id: OwnedRoomId, } macro_rules! impl_possibly_redacted_event { ( $ty:ident ( $content_trait:ident, $redacted_content_trait:ident, $event_type:ident ) $( where C::Redacted: $trait:ident, )? { $($extra:tt)* } ) => { impl $ty where C: $content_trait + RedactContent, C::Redacted: $redacted_content_trait, $( C::Redacted: $trait, )? { /// Returns the `type` of this event. pub fn event_type(&self) -> $event_type { match self { Self::Original(ev) => ev.content.event_type(), Self::Redacted(ev) => ev.content.event_type(), } } /// Returns this event's `event_id` field. pub fn event_id(&self) -> &EventId { match self { Self::Original(ev) => &ev.event_id, Self::Redacted(ev) => &ev.event_id, } } /// Returns this event's `sender` field. pub fn sender(&self) -> &UserId { match self { Self::Original(ev) => &ev.sender, Self::Redacted(ev) => &ev.sender, } } /// Returns this event's `origin_server_ts` field. pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { match self { Self::Original(ev) => ev.origin_server_ts, Self::Redacted(ev) => ev.origin_server_ts, } } // So the room_id method can be in the same impl block, in rustdoc $($extra)* } impl Redact for $ty where C: $content_trait + RedactContent, C::Redacted: $redacted_content_trait, $( C::Redacted: $trait, )? { type Redacted = Self; fn redact(self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) -> Self { match self { Self::Original(ev) => Self::Redacted(ev.redact(redaction, version)), Self::Redacted(ev) => Self::Redacted(ev), } } } impl<'de, C> Deserialize<'de> for $ty where C: $content_trait + RedactContent, C::Redacted: $redacted_content_trait, $( C::Redacted: $trait, )? { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?; if unsigned.and_then(|u| u.redacted_because).is_some() { Ok(Self::Redacted(from_raw_json_value(&json)?)) } else { Ok(Self::Original(from_raw_json_value(&json)?)) } } } } } impl_possibly_redacted_event!( MessageLikeEvent( MessageLikeEventContent, RedactedMessageLikeEventContent, MessageLikeEventType ) { /// Returns this event's `room_id` field. pub fn room_id(&self) -> &RoomId { match self { Self::Original(ev) => &ev.room_id, Self::Redacted(ev) => &ev.room_id, } } /// Get the inner `OriginalMessageLikeEvent` if this is an unredacted event. pub fn as_original(&self) -> Option<&OriginalMessageLikeEvent> { match self { Self::Original(v) => Some(v), _ => None, } } } ); impl_possibly_redacted_event!( SyncMessageLikeEvent( MessageLikeEventContent, RedactedMessageLikeEventContent, MessageLikeEventType ) { /// Get the inner `OriginalSyncMessageLikeEvent` if this is an unredacted event. pub fn as_original(&self) -> Option<&OriginalSyncMessageLikeEvent> { match self { Self::Original(v) => Some(v), _ => None, } } /// Convert this sync event into a full event (one with a `room_id` field). pub fn into_full_event(self, room_id: OwnedRoomId) -> MessageLikeEvent { match self { Self::Original(ev) => MessageLikeEvent::Original(ev.into_full_event(room_id)), Self::Redacted(ev) => MessageLikeEvent::Redacted(ev.into_full_event(room_id)), } } } ); impl_possibly_redacted_event!( StateEvent(StateEventContent, RedactedStateEventContent, StateEventType) where C::Redacted: StateEventContent, { /// Returns this event's `room_id` field. pub fn room_id(&self) -> &RoomId { match self { Self::Original(ev) => &ev.room_id, Self::Redacted(ev) => &ev.room_id, } } /// Returns this event's `state_key` field. pub fn state_key(&self) -> &C::StateKey { match self { Self::Original(ev) => &ev.state_key, Self::Redacted(ev) => &ev.state_key, } } /// Get the inner `OriginalStateEvent` if this is an unredacted event. pub fn as_original(&self) -> Option<&OriginalStateEvent> { match self { Self::Original(v) => Some(v), _ => None, } } } ); impl_possibly_redacted_event!( SyncStateEvent(StateEventContent, RedactedStateEventContent, StateEventType) where C::Redacted: StateEventContent, { /// Returns this event's `state_key` field. pub fn state_key(&self) -> &C::StateKey { match self { Self::Original(ev) => &ev.state_key, Self::Redacted(ev) => &ev.state_key, } } /// Get the inner `OriginalSyncStateEvent` if this is an unredacted event. pub fn as_original(&self) -> Option<&OriginalSyncStateEvent> { match self { Self::Original(v) => Some(v), _ => None, } } /// Convert this sync event into a full event (one with a `room_id` field). pub fn into_full_event(self, room_id: OwnedRoomId) -> StateEvent { match self { Self::Original(ev) => StateEvent::Original(ev.into_full_event(room_id)), Self::Redacted(ev) => StateEvent::Redacted(ev.into_full_event(room_id)), } } } ); macro_rules! impl_sync_from_full { ($ty:ident, $full:ident, $content_trait:ident, $redacted_content_trait: ident) => { impl From<$full> for $ty where C: $content_trait + RedactContent, C::Redacted: $redacted_content_trait, { fn from(full: $full) -> Self { match full { $full::Original(ev) => Self::Original(ev.into()), $full::Redacted(ev) => Self::Redacted(ev.into()), } } } }; } impl_sync_from_full!( SyncMessageLikeEvent, MessageLikeEvent, MessageLikeEventContent, RedactedMessageLikeEventContent ); impl_sync_from_full!(SyncStateEvent, StateEvent, StateEventContent, RedactedStateEventContent); ruma-common-0.10.5/src/events/location/zoomlevel_serde.rs000064400000000000000000000010431046102023000216060ustar 00000000000000//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). use js_int::UInt; use serde::{de, Deserialize}; use super::{ZoomLevel, ZoomLevelError}; impl<'de> Deserialize<'de> for ZoomLevel { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let uint = UInt::deserialize(deserializer)?; if uint > Self::MAX.into() { Err(de::Error::custom(ZoomLevelError::TooHigh)) } else { Ok(Self(uint)) } } } ruma-common-0.10.5/src/events/location.rs000064400000000000000000000152411046102023000164150ustar 00000000000000//! Types for extensible location message events ([MSC3488]). //! //! [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 use js_int::UInt; use ruma_macros::{EventContent, StringEnum}; use serde::{Deserialize, Serialize}; mod zoomlevel_serde; use super::{ message::MessageContent, room::message::{LocationMessageEventContent, MessageType, Relation, RoomMessageEventContent}, }; use crate::{MilliSecondsSinceUnixEpoch, PrivOwnedStr}; /// The payload for an extensible location message. /// /// This is the new primary type introduced in [MSC3488] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `LocationEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Location`]. You can convert it back with /// [`LocationEventContent::from_location_room_message()`]. /// /// [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Location`]: super::room::message::MessageType::Location #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.location", kind = MessageLike)] pub struct LocationEventContent { /// The text representation of the message. #[serde(flatten)] pub message: MessageContent, /// The location info of the message. #[serde(rename = "m.location")] pub location: LocationContent, /// The asset this message refers to. #[serde(default, rename = "m.asset", skip_serializing_if = "ruma_common::serde::is_default")] pub asset: AssetContent, /// The timestamp this message refers to. #[serde(rename = "m.ts", skip_serializing_if = "Option::is_none")] pub ts: Option, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl LocationEventContent { /// Creates a new `LocationEventContent` with the given plain text representation and location. pub fn plain(message: impl Into, location: LocationContent) -> Self { Self { message: MessageContent::plain(message), location, asset: Default::default(), ts: None, relates_to: None, } } /// Creates a new `LocationEventContent` with the given text representation and location. pub fn with_message(message: MessageContent, location: LocationContent) -> Self { Self { message, location, asset: Default::default(), ts: None, relates_to: None } } /// Create a new `LocationEventContent` from the given `LocationMessageEventContent` and /// optional relation. pub fn from_location_room_message( content: LocationMessageEventContent, relates_to: Option, ) -> Self { let LocationMessageEventContent { body, geo_uri, message, location, asset, ts, .. } = content; let message = message.unwrap_or_else(|| MessageContent::plain(body)); let location = location.unwrap_or_else(|| LocationContent::new(geo_uri)); let asset = asset.unwrap_or_default(); Self { message, location, asset, ts, relates_to } } } impl From for RoomMessageEventContent { fn from(content: LocationEventContent) -> Self { let LocationEventContent { message, location, asset, ts, relates_to } = content; Self { msgtype: MessageType::Location(LocationMessageEventContent::from_extensible_content( message, location, asset, ts, )), relates_to, } } } /// Location content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct LocationContent { /// A `geo:` URI representing the location. /// /// See [RFC 5870](https://datatracker.ietf.org/doc/html/rfc5870) for more details. pub uri: String, /// The description of the location. /// /// It should be used to label the location on a map. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// A zoom level to specify the displayed area size. #[serde(skip_serializing_if = "Option::is_none")] pub zoom_level: Option, } impl LocationContent { /// Creates a new `LocationContent` with the given geo URI. pub fn new(uri: String) -> Self { Self { uri, description: None, zoom_level: None } } } /// An error encountered when trying to convert to a `ZoomLevel`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum ZoomLevelError { /// The value is higher than [`ZoomLevel::MAX`]. #[error("value too high")] TooHigh, } /// A zoom level. /// /// This is an integer between 0 and 20 as defined in the [OpenStreetMap Wiki]. /// /// [OpenStreetMap Wiki]: https://wiki.openstreetmap.org/wiki/Zoom_levels #[derive(Clone, Debug, Serialize)] pub struct ZoomLevel(UInt); impl ZoomLevel { /// The smallest value of a `ZoomLevel`, 0. pub const MIN: u8 = 0; /// The largest value of a `ZoomLevel`, 20. pub const MAX: u8 = 20; /// Creates a new `ZoomLevel` with the given value. pub fn new(value: u8) -> Option { if value > Self::MAX { None } else { Some(Self(value.into())) } } /// The value of this `ZoomLevel`. pub fn get(&self) -> UInt { self.0 } } impl TryFrom for ZoomLevel { type Error = ZoomLevelError; fn try_from(value: u8) -> Result { Self::new(value).ok_or(ZoomLevelError::TooHigh) } } /// Asset content. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct AssetContent { /// The type of asset being referred to. #[serde(rename = "type")] pub type_: AssetType, } impl AssetContent { /// Creates a new default `AssetContent`. pub fn new() -> Self { Self::default() } } /// The type of an asset. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] pub enum AssetType { /// The asset is the sender of the event. #[ruma_enum(rename = "m.self")] Self_, #[doc(hidden)] _Custom(PrivOwnedStr), } impl Default for AssetType { fn default() -> Self { Self::Self_ } } ruma-common-0.10.5/src/events/message/content_serde.rs000064400000000000000000000104201046102023000210570ustar 00000000000000//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). use serde::{ser::SerializeStruct, Deserialize, Serialize}; use super::{MessageContent, Text, TryFromExtensibleError}; #[derive(Debug, Default, Deserialize)] pub(crate) struct MessageContentSerDeHelper { /// Plain text short form, stable name. #[serde(rename = "m.text")] text_stable: Option, /// Plain text short form, unstable name. #[serde(rename = "org.matrix.msc1767.text")] text_unstable: Option, /// HTML short form, stable name. #[serde(rename = "m.html")] html_stable: Option, /// HTML short form, unstable name. #[serde(rename = "org.matrix.msc1767.html")] html_unstable: Option, /// Long form, stable name. #[serde(rename = "m.message")] message_stable: Option>, /// Long form, unstable name. #[serde(rename = "org.matrix.msc1767.message")] message_unstable: Option>, } impl TryFrom for MessageContent { type Error = TryFromExtensibleError; fn try_from(helper: MessageContentSerDeHelper) -> Result { let MessageContentSerDeHelper { text_stable, text_unstable, html_stable, html_unstable, message_stable, message_unstable, } = helper; if let Some(message) = message_stable.or(message_unstable) { Ok(Self(message)) } else { let message: Vec<_> = html_stable .or(html_unstable) .map(Text::html) .into_iter() .chain(text_stable.or(text_unstable).map(Text::plain)) .collect(); if !message.is_empty() { Ok(Self(message)) } else { Err(TryFromExtensibleError::MissingField("m.message, m.text or m.html".to_owned())) } } } } impl Serialize for MessageContent { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { #[cfg(feature = "unstable-msc3554")] let has_shortcut = |message: &Text| { matches!(&*message.mimetype, "text/plain" | "text/html") && message.lang.is_none() }; #[cfg(not(feature = "unstable-msc3554"))] let has_shortcut = |message: &Text| matches!(&*message.mimetype, "text/plain" | "text/html"); if self.iter().all(has_shortcut) { let mut st = serializer.serialize_struct("MessageContent", self.len())?; for message in self.iter() { if message.mimetype == "text/plain" { st.serialize_field("org.matrix.msc1767.text", &message.body)?; } else if message.mimetype == "text/html" { st.serialize_field("org.matrix.msc1767.html", &message.body)?; } } st.end() } else { let mut st = serializer.serialize_struct("MessageContent", 1)?; st.serialize_field("org.matrix.msc1767.message", &self.0)?; st.end() } } } pub(crate) mod as_vec { use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serializer}; use crate::events::message::{MessageContent, Text}; /// Serializes a `Option` as a `Vec`. pub fn serialize(content: &Option, serializer: S) -> Result where S: Serializer, { if let Some(content) = content { let mut seq = serializer.serialize_seq(Some(content.len()))?; for e in content.iter() { seq.serialize_element(e)?; } seq.end() } else { serializer.serialize_seq(Some(0))?.end() } } /// Deserializes a `Vec` to an `Option`. pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { Option::>::deserialize(deserializer).and_then(|content| { content.map(MessageContent::new).ok_or_else(|| { de::Error::invalid_value(de::Unexpected::Other("empty array"), &"a non-empty array") }) }) } } ruma-common-0.10.5/src/events/message.rs000064400000000000000000000273751046102023000162440ustar 00000000000000//! Types for extensible text message events ([MSC1767]). //! //! # Extensible events //! //! MSCs [1767] (Text, Emote and Notice), [3551] (Files), [3552] (Images and Stickers), [3553] //! (Videos), [3246] (Audio), and [3488] (Location) introduce new primary types called extensible //! events. These types are meant to replace the `m.room.message` primary type and its `msgtype`s. //! Other MSCs introduce new types with an `m.room.message` fallback, like [MSC3245] (Voice //! Messages), and types that only have an extensible events format, like [MSC3381] (Polls). //! //! # Transition Period //! //! MSC1767 defines a transition period that will start after the extensible events are released in //! a Matrix version. It should last approximately one year, but the end of that period will be //! formalized in a new Matrix version. //! //! The new primary types should not be sent over the Matrix network before the end of the //! transition period. Instead, transitional `m.room.message` events should be sent. These //! transitional events include the content of the now legacy `m.room.message` event and the content //! of the new extensible event types in a single event. //! //! # How to use them //! //! First, you can enable the `unstable-extensible-events` feature from the `ruma` crate, that //! will enable all the MSCs for the extensible events that correspond to the legacy `msgtype`s //! (1767, 3246, 3488, 3551, 3552, 3553). It is also possible to enable only the MSCs you want with //! the `unstable-mscXXXX` features (where `XXXX` is the number of the MSC). //! //! The recommended way to send transitional extensible events while they are unstable and during //! the transition period is to build one of the new primary types and then to convert it to a //! [`RoomMessageEventContent`] by using `.into()` or `RoomMessageEventContent::from()`. The //! provided constructors will copy the relevant data in the legacy fields. //! //! For incoming events, a `RoomMessageEventContent` can be converted to an extensible event with //! the relevant `from_*_room_message` method on the primary type. This conversion will work even //! with legacy `m.room.message` events that don't have extensible events content. //! //! It is also possible to enable extensible events support and continue using //! `RoomMessageEventContent`'s constructors. The data will be duplicated in both the legacy and //! extensible events fields. //! //! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 //! [1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 //! [3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 //! [3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 //! [3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 //! [3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246 //! [3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 //! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 //! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381 use std::ops::Deref; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use thiserror::Error; pub(crate) mod content_serde; use content_serde::MessageContentSerDeHelper; use super::room::message::{ FormattedBody, MessageFormat, MessageType, Relation, RoomMessageEventContent, TextMessageEventContent, }; /// The payload for an extensible text message. /// /// This is the new primary type introduced in [MSC1767] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// To construct a `MessageEventContent` with a custom [`MessageContent`], convert it with /// `MessageEventContent::from()` / `.into()`. /// /// `MessageEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Text`]. You can convert it back with /// [`MessageEventContent::from_text_room_message()`]. /// /// [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Text`]: super::room::message::MessageType::Text #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.message", kind = MessageLike)] pub struct MessageEventContent { /// The message's text content. #[serde(flatten)] pub message: MessageContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl MessageEventContent { /// A convenience constructor to create a plain text message. pub fn plain(body: impl Into) -> Self { Self { message: MessageContent::plain(body), relates_to: None } } /// A convenience constructor to create an HTML message. pub fn html(body: impl Into, html_body: impl Into) -> Self { Self { message: MessageContent::html(body, html_body), relates_to: None } } /// A convenience constructor to create a Markdown message. /// /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain /// text message. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } /// Create a new `MessageEventContent` from the given `TextMessageEventContent` and optional /// relation. pub fn from_text_room_message( content: TextMessageEventContent, relates_to: Option, ) -> Self { let TextMessageEventContent { body, formatted, message, .. } = content; if let Some(message) = message { Self { message, relates_to } } else { Self { message: MessageContent::from_room_message_content(body, formatted), relates_to } } } } impl From for MessageEventContent { fn from(message: MessageContent) -> Self { Self { message, relates_to: None } } } impl From for RoomMessageEventContent { fn from(content: MessageEventContent) -> Self { let MessageEventContent { message, relates_to, .. } = content; Self { msgtype: MessageType::Text(message.into()), relates_to } } } /// Text message content. /// /// A `MessageContent` must contain at least one message to be used as a fallback text /// representation. /// /// To construct a `MessageContent` with custom MIME types, construct a `Vec` first and use /// its `.try_from()` / `.try_into()` implementation that will only fail if the `Vec` is empty. #[derive(Clone, Debug, Deserialize)] #[serde(try_from = "MessageContentSerDeHelper")] pub struct MessageContent(pub(crate) Vec); impl MessageContent { /// Create a `MessageContent` from an array of messages. /// /// Returns `None` if the array is empty. pub fn new(messages: Vec) -> Option { if messages.is_empty() { None } else { Some(Self(messages)) } } /// A convenience constructor to create a plain text message. pub fn plain(body: impl Into) -> Self { Self(vec![Text::plain(body)]) } /// A convenience constructor to create an HTML message. pub fn html(body: impl Into, html_body: impl Into) -> Self { Self(vec![Text::html(html_body), Text::plain(body)]) } /// A convenience constructor to create a Markdown message. /// /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain /// text message. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { let mut message = Vec::with_capacity(2); if let Some(html_body) = Text::markdown(&body) { message.push(html_body); } message.push(Text::plain(body)); Self(message) } /// Create a new `MessageContent` from the given body and optional formatted body. pub(crate) fn from_room_message_content( body: String, formatted: Option, ) -> Self { if let Some(FormattedBody { body: html_body, .. }) = formatted.filter(|formatted| formatted.format == MessageFormat::Html) { Self::html(body, html_body) } else { Self::plain(body) } } /// Get the plain text representation of this message. pub fn find_plain(&self) -> Option<&str> { self.iter() .find(|content| content.mimetype == "text/plain") .map(|content| content.body.as_ref()) } /// Get the HTML representation of this message. pub fn find_html(&self) -> Option<&str> { self.iter() .find(|content| content.mimetype == "text/html") .map(|content| content.body.as_ref()) } } /// The error type returned when trying to construct an empty `MessageContent`. #[derive(Debug, Error)] #[non_exhaustive] #[error("MessageContent cannot be empty")] pub struct EmptyMessageContentError; impl TryFrom> for MessageContent { type Error = EmptyMessageContentError; fn try_from(messages: Vec) -> Result { Self::new(messages).ok_or(EmptyMessageContentError) } } impl Deref for MessageContent { type Target = [Text]; fn deref(&self) -> &Self::Target { &self.0 } } /// Text message content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Text { /// The MIME type of the `body`. /// /// This must follow the format defined in [RFC6838]. /// /// [RFC6838]: https://datatracker.ietf.org/doc/html/rfc6838 #[serde(default = "Text::default_mimetype")] pub mimetype: String, /// The text content. pub body: String, /// The language of the text ([MSC3554]). /// /// This must be a valid language code according to [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt). /// /// [MSC3554]: https://github.com/matrix-org/matrix-spec-proposals/pull/3554 #[cfg(feature = "unstable-msc3554")] #[serde(skip_serializing_if = "Option::is_none")] pub lang: Option, } impl Text { /// Creates a new `Text` with the given MIME type and body. pub fn new(mimetype: impl Into, body: impl Into) -> Self { Self { mimetype: mimetype.into(), body: body.into(), #[cfg(feature = "unstable-msc3554")] lang: None, } } /// Creates a new plain text message body. pub fn plain(body: impl Into) -> Self { Self::new("text/plain", body) } /// Creates a new HTML-formatted message body. pub fn html(body: impl Into) -> Self { Self::new("text/html", body) } /// Creates a new HTML-formatted message body by parsing the Markdown in `body`. /// /// Returns `None` if no Markdown formatting was found. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef) -> Option { let body = body.as_ref(); let mut html_body = String::new(); pulldown_cmark::html::push_html(&mut html_body, pulldown_cmark::Parser::new(body)); (html_body != format!("

{}

\n", body)).then(|| Self::html(html_body)) } fn default_mimetype() -> String { "text/plain".to_owned() } } /// The error type returned when a conversion to an extensible event type fails. #[derive(Debug, Error)] #[non_exhaustive] pub enum TryFromExtensibleError { /// A field is missing. #[error("missing field `{0}`")] MissingField(String), } ruma-common-0.10.5/src/events/notice.rs000064400000000000000000000061361046102023000160710ustar 00000000000000//! Types for extensible notice message events ([MSC1767]). //! //! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ message::MessageContent, room::message::{MessageType, NoticeMessageEventContent, Relation, RoomMessageEventContent}, }; /// The payload for an extensible notice message. /// /// This is the new primary type introduced in [MSC1767] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `NoticeEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Notice`]. You can convert it back with /// [`NoticeEventContent::from_notice_room_message()`]. /// /// [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Notice`]: super::room::message::MessageType::Notice #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.notice", kind = MessageLike)] pub struct NoticeEventContent { /// The message's text content. #[serde(flatten)] pub message: MessageContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl NoticeEventContent { /// A convenience constructor to create a plain text notice. pub fn plain(body: impl Into) -> Self { Self { message: MessageContent::plain(body), relates_to: None } } /// A convenience constructor to create an HTML notice. pub fn html(body: impl Into, html_body: impl Into) -> Self { Self { message: MessageContent::html(body, html_body), relates_to: None } } /// A convenience constructor to create a Markdown notice. /// /// Returns an HTML notice if some Markdown formatting was detected, otherwise returns a plain /// text notice. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } /// Create a new `NoticeEventContent` from the given `NoticeMessageEventContent` and optional /// relation. pub fn from_notice_room_message( content: NoticeMessageEventContent, relates_to: Option, ) -> Self { let NoticeMessageEventContent { body, formatted, message, .. } = content; if let Some(message) = message { Self { message, relates_to } } else { Self { message: MessageContent::from_room_message_content(body, formatted), relates_to } } } } impl From for RoomMessageEventContent { fn from(content: NoticeEventContent) -> Self { let NoticeEventContent { message, relates_to, .. } = content; Self { msgtype: MessageType::Notice(message.into()), relates_to } } } ruma-common-0.10.5/src/events/pdu.rs000064400000000000000000000134521046102023000153770ustar 00000000000000//! Types for persistent data unit schemas //! //! The differences between the `RoomV1Pdu` schema and the `RoomV3Pdu` schema are that the //! `RoomV1Pdu` takes an `event_id` field (`RoomV3Pdu` does not), and `auth_events` and //! `prev_events` take `Vec<(OwnedEventId, EventHash)> rather than `Vec` in //! `RoomV3Pdu`. use std::collections::BTreeMap; use js_int::UInt; use serde::{ de::{Error as _, IgnoredAny}, Deserialize, Deserializer, Serialize, }; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; use super::RoomEventType; use crate::{ MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, }; /// Enum for PDU schemas #[derive(Clone, Debug, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(untagged)] pub enum Pdu { /// PDU for room versions 1 and 2. RoomV1Pdu(RoomV1Pdu), /// PDU for room versions 3 and above. RoomV3Pdu(RoomV3Pdu), } /// A 'persistent data unit' (event) for room versions 1 and 2. #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct RoomV1Pdu { /// Event ID for the PDU. pub event_id: OwnedEventId, /// The room this event belongs to. pub room_id: OwnedRoomId, /// The user id of the user who sent this event. pub sender: OwnedUserId, /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver /// of when this event was created. pub origin_server_ts: MilliSecondsSinceUnixEpoch, // TODO: Encode event type as content enum variant, like event enums do /// The event's type. #[serde(rename = "type")] pub kind: RoomEventType, /// The event's content. pub content: Box, /// A key that determines which piece of room state the event represents. #[serde(skip_serializing_if = "Option::is_none")] pub state_key: Option, /// Event IDs for the most recent events in the room that the homeserver was /// aware of when it created this event. #[serde(skip_serializing_if = "Vec::is_empty")] pub prev_events: Vec<(OwnedEventId, EventHash)>, /// The maximum depth of the `prev_events`, plus one. pub depth: UInt, /// Event IDs for the authorization events that would allow this event to be /// in the room. #[serde(skip_serializing_if = "Vec::is_empty")] pub auth_events: Vec<(OwnedEventId, EventHash)>, /// For redaction events, the ID of the event being redacted. #[serde(skip_serializing_if = "Option::is_none")] pub redacts: Option, /// Additional data added by the origin server but not covered by the /// signatures. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub unsigned: BTreeMap>, /// Content hashes of the PDU. pub hashes: EventHash, /// Signatures for the PDU. pub signatures: BTreeMap>, } /// A 'persistent data unit' (event) for room versions 3 and beyond. #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct RoomV3Pdu { /// The room this event belongs to. pub room_id: OwnedRoomId, /// The user id of the user who sent this event. pub sender: OwnedUserId, /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver /// of when this event was created. pub origin_server_ts: MilliSecondsSinceUnixEpoch, // TODO: Encode event type as content enum variant, like event enums do /// The event's type. #[serde(rename = "type")] pub kind: RoomEventType, /// The event's content. pub content: Box, /// A key that determines which piece of room state the event represents. #[serde(skip_serializing_if = "Option::is_none")] pub state_key: Option, /// Event IDs for the most recent events in the room that the homeserver was /// aware of when it created this event. pub prev_events: Vec, /// The maximum depth of the `prev_events`, plus one. pub depth: UInt, /// Event IDs for the authorization events that would allow this event to be /// in the room. pub auth_events: Vec, /// For redaction events, the ID of the event being redacted. #[serde(skip_serializing_if = "Option::is_none")] pub redacts: Option, /// Additional data added by the origin server but not covered by the /// signatures. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub unsigned: BTreeMap>, /// Content hashes of the PDU. pub hashes: EventHash, /// Signatures for the PDU. pub signatures: BTreeMap>, } /// Content hashes of a PDU. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct EventHash { /// The SHA-256 hash. pub sha256: String, } impl EventHash { /// Create a new `EventHash` with the given SHA256 hash. pub fn new(sha256: String) -> Self { Self { sha256 } } } impl<'de> Deserialize<'de> for Pdu { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct GetEventId { event_id: Option, } let json = Box::::deserialize(deserializer)?; if from_json_str::(json.get()).map_err(D::Error::custom)?.event_id.is_some() { from_json_str(json.get()).map(Self::RoomV1Pdu).map_err(D::Error::custom) } else { from_json_str(json.get()).map(Self::RoomV3Pdu).map_err(D::Error::custom) } } } ruma-common-0.10.5/src/events/policy/rule/room.rs000064400000000000000000000063171046102023000200330ustar 00000000000000//! Types for the [`m.policy.rule.room`] event. //! //! [`m.policy.rule.room`]: https://spec.matrix.org/v1.2/client-server-api/#mpolicyruleroom use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::PolicyRuleEventContent; /// The content of an `m.policy.rule.room` event. /// /// This event type is used to apply rules to room entities. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] #[ruma_event(type = "m.policy.rule.room", kind = State, state_key_type = String)] pub struct PolicyRuleRoomEventContent(pub PolicyRuleEventContent); #[cfg(test)] mod tests { use js_int::int; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{OriginalPolicyRuleRoomEvent, PolicyRuleRoomEventContent}; use crate::{ event_id, events::{ policy::rule::{PolicyRuleEventContent, Recommendation}, StateUnsigned, }, room_id, serde::Raw, user_id, MilliSecondsSinceUnixEpoch, }; #[test] fn serialization() { let room_event = OriginalPolicyRuleRoomEvent { event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), sender: user_id!("@example:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(1_432_735_824_653_u64.try_into().unwrap()), room_id: room_id!("!jEsUZKDJdhlrceRyVU:example.org").to_owned(), state_key: "rule:#*:example.org".into(), unsigned: StateUnsigned { age: Some(int!(1234)), ..StateUnsigned::default() }, content: PolicyRuleRoomEventContent(PolicyRuleEventContent { entity: "#*:example.org".into(), reason: "undesirable content".into(), recommendation: Recommendation::Ban, }), }; let json = json!({ "content": { "entity": "#*:example.org", "reason": "undesirable content", "recommendation": "m.ban" }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 1_432_735_824_653_u64, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "state_key": "rule:#*:example.org", "type": "m.policy.rule.room", "unsigned": { "age": 1234 } }); assert_eq!(to_json_value(room_event).unwrap(), json); } #[test] fn deserialization() { let json = json!({ "content": { "entity": "#*:example.org", "reason": "undesirable content", "recommendation": "m.ban" }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 1_432_735_824_653_u64, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "state_key": "rule:#*:example.org", "type": "m.policy.rule.room", "unsigned": { "age": 1234 } }); from_json_value::>(json).unwrap().deserialize().unwrap(); } } ruma-common-0.10.5/src/events/policy/rule/server.rs000064400000000000000000000011631046102023000203570ustar 00000000000000//! Types for the [`m.policy.rule.server`] event. //! //! [`m.policy.rule.server`]: https://spec.matrix.org/v1.2/client-server-api/#mpolicyruleserver use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::PolicyRuleEventContent; /// The content of an `m.policy.rule.server` event. /// /// This event type is used to apply rules to server entities. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] #[ruma_event(type = "m.policy.rule.server", kind = State, state_key_type = String)] pub struct PolicyRuleServerEventContent(pub PolicyRuleEventContent); ruma-common-0.10.5/src/events/policy/rule/user.rs000064400000000000000000000011451046102023000200270ustar 00000000000000//! Types for the [`m.policy.rule.user`] event. //! //! [`m.policy.rule.user`]: https://spec.matrix.org/v1.2/client-server-api/#mpolicyruleuser use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::PolicyRuleEventContent; /// The content of an `m.policy.rule.user` event. /// /// This event type is used to apply rules to user entities. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] #[ruma_event(type = "m.policy.rule.user", kind = State, state_key_type = String)] pub struct PolicyRuleUserEventContent(pub PolicyRuleEventContent); ruma-common-0.10.5/src/events/policy/rule.rs000064400000000000000000000026471046102023000170610ustar 00000000000000//! Modules and types for events in the `m.policy.rule` namespace. use serde::{Deserialize, Serialize}; use crate::{serde::StringEnum, PrivOwnedStr}; pub mod room; pub mod server; pub mod user; /// The payload for policy rule events. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PolicyRuleEventContent { /// The entity affected by this rule. /// /// Glob characters `*` and `?` can be used to match zero or more characters or exactly one /// character respectively. pub entity: String, /// The suggested action to take. pub recommendation: Recommendation, /// The human-readable description for the recommendation. pub reason: String, } impl PolicyRuleEventContent { /// Creates a new `PolicyRuleEventContent` with the given entity, recommendation and reason. pub fn new(entity: String, recommendation: Recommendation, reason: String) -> Self { Self { entity, recommendation, reason } } } /// The possible actions that can be taken. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum Recommendation { /// Entities affected by the rule should be banned from participation where possible. #[ruma_enum(rename = "m.ban")] Ban, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/events/policy.rs000064400000000000000000000001031046102023000160730ustar 00000000000000//! Modules for events in the `m.policy` namespace. pub mod rule; ruma-common-0.10.5/src/events/poll/end.rs000064400000000000000000000026201046102023000163160ustar 00000000000000//! Types for the [`m.poll.end`] event. use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::ReferenceRelation; use crate::OwnedEventId; /// The payload for a poll end event. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "org.matrix.msc3381.poll.end", alias = "m.poll.end", kind = MessageLike)] pub struct PollEndEventContent { /// The poll end content of the message. #[serde(rename = "org.matrix.msc3381.poll.end", alias = "m.poll.end")] pub poll_end: PollEndContent, /// Information about the poll start event this responds to. #[serde(rename = "m.relates_to")] pub relates_to: ReferenceRelation, } impl PollEndEventContent { /// Creates a new `PollEndEventContent` that responds to the given poll start event ID, /// with the given poll end content. pub fn new(poll_end: PollEndContent, poll_start_id: OwnedEventId) -> Self { Self { poll_end, relates_to: ReferenceRelation::new(poll_start_id) } } } /// Poll end content. /// /// This is currently empty. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollEndContent {} impl PollEndContent { /// Creates a new empty `PollEndContent`. pub fn new() -> Self { Self {} } } ruma-common-0.10.5/src/events/poll/response.rs000064400000000000000000000034421046102023000174110ustar 00000000000000//! Types for the [`m.poll.response`] event. use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::ReferenceRelation; use crate::OwnedEventId; /// The payload for a poll response event. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "org.matrix.msc3381.poll.response", alias = "m.poll.response", kind = MessageLike)] pub struct PollResponseEventContent { /// The poll response content of the message. #[serde(rename = "org.matrix.msc3381.poll.response", alias = "m.poll.response")] pub poll_response: PollResponseContent, /// Information about the poll start event this responds to. #[serde(rename = "m.relates_to")] pub relates_to: ReferenceRelation, } impl PollResponseEventContent { /// Creates a new `PollResponseEventContent` that responds to the given poll start event ID, /// with the given poll response content. pub fn new(poll_response: PollResponseContent, poll_start_id: OwnedEventId) -> Self { Self { poll_response, relates_to: ReferenceRelation::new(poll_start_id) } } } /// Poll response content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollResponseContent { /// The IDs of the selected answers of the poll. /// /// It should be truncated to `max_selections` from the related poll start event. /// /// If this is an empty array or includes unknown IDs, this vote should be considered as /// spoiled. pub answers: Vec, } impl PollResponseContent { /// Creates a new `PollResponseContent` with the given answers. pub fn new(answers: Vec) -> Self { Self { answers } } } ruma-common-0.10.5/src/events/poll/start/poll_answers_serde.rs000064400000000000000000000010601046102023000225740ustar 00000000000000//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). use serde::Deserialize; use super::{PollAnswer, PollAnswers, PollAnswersError}; #[derive(Debug, Default, Deserialize)] pub(crate) struct PollAnswersDeHelper(Vec); impl TryFrom for PollAnswers { type Error = PollAnswersError; fn try_from(helper: PollAnswersDeHelper) -> Result { let mut answers = helper.0; answers.truncate(PollAnswers::MAX_LENGTH); PollAnswers::try_from(answers) } } ruma-common-0.10.5/src/events/poll/start.rs000064400000000000000000000124041046102023000167060ustar 00000000000000//! Types for the [`m.poll.start`] event. use js_int::{uint, UInt}; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; mod poll_answers_serde; use poll_answers_serde::PollAnswersDeHelper; use crate::{events::message::MessageContent, serde::StringEnum, PrivOwnedStr}; /// The payload for a poll start event. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "org.matrix.msc3381.poll.start", alias = "m.poll.start", kind = MessageLike)] pub struct PollStartEventContent { /// The poll start content of the message. #[serde(rename = "org.matrix.msc3381.poll.start", alias = "m.poll.start")] pub poll_start: PollStartContent, /// Optional fallback text representation of the message, for clients that don't support polls. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, } impl PollStartEventContent { /// Creates a new `PollStartEventContent` with the given poll start content. pub fn new(poll_start: PollStartContent) -> Self { Self { poll_start, message: None } } } /// Poll start content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollStartContent { /// The question of the poll. pub question: MessageContent, /// The kind of the poll. #[serde(default)] pub kind: PollKind, /// The maximum number of responses a user is able to select. /// /// Must be greater or equal to `1`. /// /// Defaults to `1`. #[serde( default = "PollStartContent::default_max_selections", skip_serializing_if = "PollStartContent::max_selections_is_default" )] pub max_selections: UInt, /// The possible answers to the poll. pub answers: PollAnswers, } impl PollStartContent { /// Creates a new `PollStartContent` with the given question, kind, and answers. pub fn new(question: MessageContent, kind: PollKind, answers: PollAnswers) -> Self { Self { question, kind, max_selections: Self::default_max_selections(), answers } } fn default_max_selections() -> UInt { uint!(1) } fn max_selections_is_default(max_selections: &UInt) -> bool { max_selections == &Self::default_max_selections() } } /// The kind of poll. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum PollKind { /// The results are revealed once the poll is closed. #[ruma_enum(rename = "org.matrix.msc3381.poll.undisclosed", alias = "m.poll.undisclosed")] Undisclosed, /// The votes are visible up until and including when the poll is closed. #[ruma_enum(rename = "org.matrix.msc3381.poll.disclosed", alias = "m.poll.disclosed")] Disclosed, #[doc(hidden)] _Custom(PrivOwnedStr), } impl Default for PollKind { fn default() -> Self { Self::Undisclosed } } /// The answers to a poll. /// /// Must include between 1 and 20 `PollAnswer`s. /// /// To build this, use the `TryFrom` implementations. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "PollAnswersDeHelper")] pub struct PollAnswers(Vec); impl PollAnswers { /// The smallest number of values contained in a `PollAnswers`. pub const MIN_LENGTH: usize = 1; /// The largest number of values contained in a `PollAnswers`. pub const MAX_LENGTH: usize = 20; /// The answers of this `PollAnswers`. pub fn answers(&self) -> &[PollAnswer] { &self.0 } } /// An error encountered when trying to convert to a `PollAnswers`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum PollAnswersError { /// There are more than [`PollAnswers::MAX_LENGTH`] values. #[error("too many values")] TooManyValues, /// There are less that [`PollAnswers::MIN_LENGTH`] values. #[error("not enough values")] NotEnoughValues, } impl TryFrom> for PollAnswers { type Error = PollAnswersError; fn try_from(value: Vec) -> Result { if value.len() < Self::MIN_LENGTH { Err(PollAnswersError::NotEnoughValues) } else if value.len() > Self::MAX_LENGTH { Err(PollAnswersError::TooManyValues) } else { Ok(Self(value)) } } } impl TryFrom<&[PollAnswer]> for PollAnswers { type Error = PollAnswersError; fn try_from(value: &[PollAnswer]) -> Result { Self::try_from(value.to_owned()) } } /// Poll answer. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollAnswer { /// The ID of the answer. /// /// This must be unique among the answers of a poll. pub id: String, /// The text representation of the answer. #[serde(flatten)] pub answer: MessageContent, } impl PollAnswer { /// Creates a new `PollAnswer` with the given id and text representation. pub fn new(id: String, answer: MessageContent) -> Self { Self { id, answer } } } ruma-common-0.10.5/src/events/poll.rs000064400000000000000000000015041046102023000155500ustar 00000000000000//! Modules for events in the `m.poll` namespace ([MSC3381]). //! //! This module also contains types shared by events in its child namespaces. //! //! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381 use serde::{Deserialize, Serialize}; use crate::OwnedEventId; pub mod end; pub mod response; pub mod start; /// An `m.reference` relation. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "rel_type", rename = "m.reference")] pub struct ReferenceRelation { /// The ID of the event this references. pub event_id: OwnedEventId, } impl ReferenceRelation { /// Creates a new `ReferenceRelation` that references the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } ruma-common-0.10.5/src/events/presence.rs000064400000000000000000000133731046102023000164150ustar 00000000000000//! A presence event is represented by a struct with a set content field. //! //! The only content valid for this event is `PresenceEventContent`. use js_int::UInt; use ruma_macros::{Event, EventContent}; use serde::{Deserialize, Serialize}; use super::{EventKind, StaticEventContent}; use crate::{presence::PresenceState, OwnedMxcUri, OwnedUserId}; /// Presence event. #[derive(Clone, Debug, Event)] #[allow(clippy::exhaustive_structs)] pub struct PresenceEvent { /// Data specific to the event type. pub content: PresenceEventContent, /// Contains the fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, } /// Informs the room of members presence. /// /// This is the only type a `PresenceEvent` can contain as its `content` field. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.presence")] pub struct PresenceEventContent { /// The current avatar URL for this user. /// /// If you activate the `compat` feature, this field being an empty string in JSON will result /// in `None` here during deserialization. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr( feature = "compat", serde(default, deserialize_with = "crate::serde::empty_string_as_none") )] pub avatar_url: Option, /// Whether or not the user is currently active. #[serde(skip_serializing_if = "Option::is_none")] pub currently_active: Option, /// The current display name for this user. #[serde(skip_serializing_if = "Option::is_none")] pub displayname: Option, /// The last time since this user performed some action, in milliseconds. #[serde(skip_serializing_if = "Option::is_none")] pub last_active_ago: Option, /// The presence state for this user. pub presence: PresenceState, /// An optional description to accompany the presence. #[serde(skip_serializing_if = "Option::is_none")] pub status_msg: Option, } impl PresenceEventContent { /// Creates a new `PresenceEventContent` with the given state. pub fn new(presence: PresenceState) -> Self { Self { avatar_url: None, currently_active: None, displayname: None, last_active_ago: None, presence, status_msg: None, } } } impl StaticEventContent for PresenceEventContent { const KIND: EventKind = EventKind::Presence; const TYPE: &'static str = "m.presence"; } #[cfg(test)] mod tests { use js_int::uint; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{PresenceEvent, PresenceEventContent}; use crate::{mxc_uri, presence::PresenceState, user_id}; #[test] fn serialization() { let event = PresenceEvent { content: PresenceEventContent { avatar_url: Some(mxc_uri!("mxc://localhost/wefuiwegh8742w").to_owned()), currently_active: Some(false), displayname: None, last_active_ago: Some(uint!(2_478_593)), presence: PresenceState::Online, status_msg: Some("Making cupcakes".into()), }, sender: user_id!("@example:localhost").to_owned(), }; let json = json!({ "content": { "avatar_url": "mxc://localhost/wefuiwegh8742w", "currently_active": false, "last_active_ago": 2_478_593, "presence": "online", "status_msg": "Making cupcakes" }, "sender": "@example:localhost", "type": "m.presence" }); assert_eq!(to_json_value(&event).unwrap(), json); } #[test] fn deserialization() { let json = json!({ "content": { "avatar_url": "mxc://localhost/wefuiwegh8742w", "currently_active": false, "last_active_ago": 2_478_593, "presence": "online", "status_msg": "Making cupcakes" }, "sender": "@example:localhost", "type": "m.presence" }); let ev = from_json_value::(json).unwrap(); assert_eq!( ev.content.avatar_url.as_deref(), Some(mxc_uri!("mxc://localhost/wefuiwegh8742w")) ); assert_eq!(ev.content.currently_active, Some(false)); assert_eq!(ev.content.displayname, None); assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593))); assert_eq!(ev.content.presence, PresenceState::Online); assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes")); assert_eq!(ev.sender, "@example:localhost"); #[cfg(feature = "compat")] { let json = json!({ "content": { "avatar_url": "", "currently_active": false, "last_active_ago": 2_478_593, "presence": "online", "status_msg": "Making cupcakes" }, "sender": "@example:localhost", "type": "m.presence" }); let ev = from_json_value::(json).unwrap(); assert_eq!(ev.content.avatar_url, None); assert_eq!(ev.content.currently_active, Some(false)); assert_eq!(ev.content.displayname, None); assert_eq!(ev.content.last_active_ago, Some(uint!(2_478_593))); assert_eq!(ev.content.presence, PresenceState::Online); assert_eq!(ev.content.status_msg.as_deref(), Some("Making cupcakes")); assert_eq!(ev.sender, "@example:localhost"); } } } ruma-common-0.10.5/src/events/push_rules.rs000064400000000000000000000221561046102023000170010ustar 00000000000000//! Types for the [`m.push_rules`] event. //! //! [`m.push_rules`]: https://spec.matrix.org/v1.2/client-server-api/#mpush_rules use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::push::Ruleset; /// The content of an `m.push_rules` event. /// /// Describes all push rules for a user. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.push_rules", kind = GlobalAccountData)] pub struct PushRulesEventContent { /// The global ruleset. pub global: Ruleset, } impl PushRulesEventContent { /// Creates a new `PushRulesEventContent` with the given global ruleset. /// /// You can also construct a `PushRulesEventContent` from a global ruleset using `From` / /// `Into`. pub fn new(global: Ruleset) -> Self { Self { global } } } impl From for PushRulesEventContent { fn from(global: Ruleset) -> Self { Self::new(global) } } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json}; use super::PushRulesEvent; #[test] fn sanity_check() { // This is a full example of a push rules event from the specification. let json_data = json!({ "content": { "global": { "content": [ { "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" } ], "default": true, "enabled": true, "pattern": "alice", "rule_id": ".m.rule.contains_user_name" } ], "override": [ { "actions": [ "dont_notify" ], "conditions": [], "default": true, "enabled": false, "rule_id": ".m.rule.master" }, { "actions": [ "dont_notify" ], "conditions": [ { "key": "content.msgtype", "kind": "event_match", "pattern": "m.notice" } ], "default": true, "enabled": true, "rule_id": ".m.rule.suppress_notices" } ], "room": [], "sender": [], "underride": [ { "actions": [ "notify", { "set_tweak": "sound", "value": "ring" }, { "set_tweak": "highlight", "value": false } ], "conditions": [ { "key": "type", "kind": "event_match", "pattern": "m.call.invite" } ], "default": true, "enabled": true, "rule_id": ".m.rule.call" }, { "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" } ], "conditions": [ { "kind": "contains_display_name" } ], "default": true, "enabled": true, "rule_id": ".m.rule.contains_display_name" }, { "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false } ], "conditions": [ { "is": "2", "kind": "room_member_count" } ], "default": true, "enabled": true, "rule_id": ".m.rule.room_one_to_one" }, { "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false } ], "conditions": [ { "key": "type", "kind": "event_match", "pattern": "m.room.member" }, { "key": "content.membership", "kind": "event_match", "pattern": "invite" }, { "key": "state_key", "kind": "event_match", "pattern": "@alice:example.com" } ], "default": true, "enabled": true, "rule_id": ".m.rule.invite_for_me" }, { "actions": [ "notify", { "set_tweak": "highlight", "value": false } ], "conditions": [ { "key": "type", "kind": "event_match", "pattern": "m.room.member" } ], "default": true, "enabled": true, "rule_id": ".m.rule.member_event" }, { "actions": [ "notify", { "set_tweak": "highlight", "value": false } ], "conditions": [ { "key": "type", "kind": "event_match", "pattern": "m.room.message" } ], "default": true, "enabled": true, "rule_id": ".m.rule.message" } ] } }, "type": "m.push_rules" }); from_json_value::(json_data).unwrap(); } } ruma-common-0.10.5/src/events/reaction.rs000064400000000000000000000047021046102023000164110ustar 00000000000000//! Types for the `m.reaction` event. use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::OwnedEventId; /// The payload for a `m.reaction` event. /// /// A reaction to another event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.reaction", kind = MessageLike)] pub struct ReactionEventContent { /// Information about the related event. #[serde(rename = "m.relates_to")] pub relates_to: Relation, } impl ReactionEventContent { /// Creates a new `ReactionEventContent` from the given relation. /// /// You can also construct a `ReactionEventContent` from a relation using `From` / `Into`. pub fn new(relates_to: Relation) -> Self { Self { relates_to } } } impl From for ReactionEventContent { fn from(relates_to: Relation) -> Self { Self::new(relates_to) } } /// Information about an annotation relation. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "rel_type", rename = "m.annotation")] pub struct Relation { /// The event that is being annotated. pub event_id: OwnedEventId, /// A string that indicates the annotation being applied. /// /// When sending emoji reactions, this field should include the colourful variation-16 when /// applicable. /// /// Clients should render reactions that have a long `key` field in a sensible manner. pub key: String, } impl Relation { /// Creates a new `Relation` with the given event ID and key. pub fn new(event_id: OwnedEventId, key: String) -> Self { Self { event_id, key } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json}; use super::ReactionEventContent; #[test] fn deserialize() { let json = json!({ "m.relates_to": { "rel_type": "m.annotation", "event_id": "$1598361704261elfgc:localhost", "key": "🦛", } }); let relates_to = assert_matches!( from_json_value::(json), Ok(ReactionEventContent { relates_to }) => relates_to ); assert_eq!(relates_to.event_id, "$1598361704261elfgc:localhost"); assert_eq!(relates_to.key, "🦛"); } } ruma-common-0.10.5/src/events/receipt.rs000064400000000000000000000076031046102023000162430ustar 00000000000000//! Types for the [`m.receipt`] event. //! //! [`m.receipt`]: https://spec.matrix.org/v1.2/client-server-api/#mreceipt use std::{ collections::BTreeMap, ops::{Deref, DerefMut}, }; use ruma_macros::{EventContent, OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, StringEnum}; use serde::{Deserialize, Serialize}; use crate::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, PrivOwnedStr, UserId}; /// The content of an `m.receipt` event. /// /// A mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of /// the event being acknowledged and *not* an ID for the receipt itself. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] #[ruma_event(type = "m.receipt", kind = EphemeralRoom)] pub struct ReceiptEventContent(pub BTreeMap); impl ReceiptEventContent { /// Get the receipt for the given user ID with the given receipt type, if it exists. pub fn user_receipt( &self, user_id: &UserId, receipt_type: ReceiptType, ) -> Option<(&EventId, &Receipt)> { self.iter().find_map(|(event_id, receipts)| { let receipt = receipts.get(&receipt_type)?.get(user_id)?; Some((event_id.as_ref(), receipt)) }) } } impl Deref for ReceiptEventContent { type Target = BTreeMap; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for ReceiptEventContent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } /// A collection of receipts. pub type Receipts = BTreeMap; /// The type of receipt. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialOrdAsRefStr, OrdAsRefStr, PartialEqAsRefStr, Eq, StringEnum)] #[non_exhaustive] pub enum ReceiptType { /// A [public read receipt]. /// /// Indicates that the given event has been presented to the user. It is /// also the point from where the unread notifications count is computed. /// /// This receipt is federated to other users. /// /// If both `Read` and `ReadPrivate` are present, the one that references /// the most recent event is used to get the latest read receipt. /// /// [public read receipt]: https://spec.matrix.org/v1.3/client-server-api/#receipts #[ruma_enum(rename = "m.read")] Read, /// A [private read receipt]. /// /// Indicates that the given event has been presented to the user. It is /// also the point from where the unread notifications count is computed. /// /// This read receipt is not federated so only the user and their homeserver /// are aware of it. /// /// If both `Read` and `ReadPrivate` are present, the one that references /// the most recent event is used to get the latest read receipt. /// /// [private read receipt]: https://github.com/matrix-org/matrix-spec-proposals/pull/2285 #[cfg(feature = "unstable-msc2285")] #[ruma_enum(rename = "org.matrix.msc2285.read.private", alias = "m.read.private")] ReadPrivate, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A mapping of user ID to receipt. /// /// The user ID is the entity who sent this receipt. pub type UserReceipts = BTreeMap; /// An acknowledgement of an event. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Receipt { /// The time when the receipt was sent. #[serde(skip_serializing_if = "Option::is_none")] pub ts: Option, } impl Receipt { /// Creates a new `Receipt` with the given timestamp. /// /// To create an empty receipt instead, use [`Receipt::default`]. pub fn new(ts: MilliSecondsSinceUnixEpoch) -> Self { Self { ts: Some(ts) } } } ruma-common-0.10.5/src/events/relation.rs000064400000000000000000000145211046102023000164220ustar 00000000000000//! Types describing [relationships between events]. //! //! [relationships between events]: https://spec.matrix.org/v1.3/client-server-api/#forming-relationships-between-events use std::fmt::Debug; #[cfg(any(feature = "unstable-msc2677", feature = "unstable-msc3440"))] use js_int::UInt; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3440")] use super::AnySyncMessageLikeEvent; #[cfg(feature = "unstable-msc3440")] use crate::serde::Raw; #[cfg(any(feature = "unstable-msc2676", feature = "unstable-msc2677"))] use crate::MilliSecondsSinceUnixEpoch; use crate::{serde::StringEnum, PrivOwnedStr}; #[cfg(feature = "unstable-msc2676")] use crate::{OwnedEventId, OwnedUserId}; /// Summary of all annotations to an event with the given key and type. #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[cfg(feature = "unstable-msc2677")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct BundledAnnotation { /// The type of the annotation. #[serde(rename = "type")] pub annotation_type: AnnotationType, /// The key used for the annotation. pub key: String, /// Time of the bundled annotation being compiled on the server. #[serde(skip_serializing_if = "Option::is_none")] pub origin_server_ts: Option, /// Number of annotations. pub count: UInt, } #[cfg(feature = "unstable-msc2677")] impl BundledAnnotation { /// Creates a new `BundledAnnotation` with the given type, key and count. pub fn new(annotation_type: AnnotationType, key: String, count: UInt) -> Self { Self { annotation_type, key, count, origin_server_ts: None } } /// Creates a new `BundledAnnotation` for a reaction with the given key and count. pub fn reaction(key: String, count: UInt) -> Self { Self::new(AnnotationType::Reaction, key, count) } } /// Type of annotation. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[cfg(feature = "unstable-msc2677")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum AnnotationType { /// A reaction. #[ruma_enum(rename = "m.reaction")] Reaction, #[doc(hidden)] _Custom(PrivOwnedStr), } /// The first chunk of annotations with a token for loading more. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg(feature = "unstable-msc2677")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct AnnotationChunk { /// The first batch of bundled annotations. pub chunk: Vec, /// Token to receive the next annotation batch. #[serde(skip_serializing_if = "Option::is_none")] pub next_batch: Option, } #[cfg(feature = "unstable-msc2677")] impl AnnotationChunk { /// Creates a new `AnnotationChunk` with the given chunk and next batch token. pub fn new(chunk: Vec, next_batch: Option) -> Self { Self { chunk, next_batch } } } /// A bundled replacement. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc2676")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct BundledReplacement { /// The ID of the replacing event. pub event_id: OwnedEventId, /// The user ID of the sender of the latest replacement. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when the latest replacement was sent. #[serde(skip_serializing_if = "Option::is_none")] pub origin_server_ts: Option, } #[cfg(feature = "unstable-msc2676")] impl BundledReplacement { /// Creates a new `BundledReplacement` with the given event ID and sender. pub fn new(event_id: OwnedEventId, sender: OwnedUserId) -> Self { Self { event_id, sender, origin_server_ts: None } } } /// A bundled thread. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct BundledThread { /// The latest event in the thread. pub latest_event: Box>, /// The number of events in the thread. pub count: UInt, /// Whether the current logged in user has participated in the thread. pub current_user_participated: bool, } #[cfg(feature = "unstable-msc3440")] impl BundledThread { /// Creates a new `BundledThread` with the given event, count and user participated flag. pub fn new( latest_event: Box>, count: UInt, current_user_participated: bool, ) -> Self { Self { latest_event, count, current_user_participated } } } /// [Bundled aggregations] of related child events. /// /// [Bundled aggregations]: https://spec.matrix.org/v1.3/client-server-api/#aggregations #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Relations { /// Annotation relations. #[cfg(feature = "unstable-msc2677")] #[serde(rename = "m.annotation")] pub annotation: Option, /// Replacement relation. #[cfg(feature = "unstable-msc2676")] #[serde(rename = "m.replace")] pub replace: Option, /// Thread relation. #[cfg(feature = "unstable-msc3440")] #[serde(rename = "io.element.thread", alias = "m.thread")] pub thread: Option, } impl Relations { /// Creates a new empty `Relations`. pub fn new() -> Self { Self::default() } } /// Relation types as defined in `rel_type` of an `m.relates_to` field. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum RelationType { /// `m.annotation`, an annotation, principally used by reactions. #[cfg(feature = "unstable-msc2677")] #[ruma_enum(rename = "m.annotation")] Annotation, /// `m.replace`, a replacement. #[cfg(feature = "unstable-msc2676")] #[ruma_enum(rename = "m.replace")] Replacement, /// `m.thread`, a participant to a thread. #[cfg(feature = "unstable-msc3440")] #[ruma_enum(rename = "io.element.thread", alias = "m.thread")] Thread, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/events/room/aliases.rs000064400000000000000000000073131046102023000172030ustar 00000000000000//! Types for the `m.room.aliases` event. use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; use crate::{ events::{ EventContent, HasDeserializeFields, RedactContent, RedactedEventContent, RedactedStateEventContent, StateEventContent, StateEventType, StateUnsigned, }, OwnedRoomAliasId, OwnedServerName, RoomVersionId, }; /// The content of an `m.room.aliases` event. /// /// Informs the room about what room aliases it has been given. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.aliases", kind = State, state_key_type = OwnedServerName, custom_redacted)] pub struct RoomAliasesEventContent { /// A list of room aliases. pub aliases: Vec, } impl RoomAliasesEventContent { /// Create an `RoomAliasesEventContent` from the given aliases. pub fn new(aliases: Vec) -> Self { Self { aliases } } } impl RedactContent for RoomAliasesEventContent { type Redacted = RedactedRoomAliasesEventContent; fn redact(self, version: &RoomVersionId) -> RedactedRoomAliasesEventContent { // We compare the long way to avoid pre version 6 behavior if/when // a new room version is introduced. let aliases = match version { RoomVersionId::V1 | RoomVersionId::V2 | RoomVersionId::V3 | RoomVersionId::V4 | RoomVersionId::V5 => Some(self.aliases), _ => None, }; RedactedRoomAliasesEventContent { aliases } } } /// An aliases event that has been redacted. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RedactedRoomAliasesEventContent { /// A list of room aliases. /// /// According to the Matrix spec version 1 redaction rules allowed this field to be /// kept after redaction, this was changed in version 6. pub aliases: Option>, } impl RedactedRoomAliasesEventContent { /// Create a `RedactedAliasesEventContent` with the given aliases. /// /// This is only valid for room version 5 and below. pub fn new_v1(aliases: Vec) -> Self { Self { aliases: Some(aliases) } } /// Create a `RedactedAliasesEventContent` with the given aliases. /// /// This is only valid for room version 6 and above. pub fn new_v6() -> Self { Self::default() } } impl EventContent for RedactedRoomAliasesEventContent { type EventType = StateEventType; fn event_type(&self) -> StateEventType { StateEventType::RoomAliases } fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result { if event_type != "m.room.aliases" { return Err(::serde::de::Error::custom(format!( "expected event type `m.room.aliases`, found `{}`", event_type ))); } serde_json::from_str(content.get()) } } impl StateEventContent for RedactedRoomAliasesEventContent { type StateKey = OwnedServerName; // FIXME: Not actually used type Unsigned = StateUnsigned; } impl RedactedStateEventContent for RedactedRoomAliasesEventContent {} // Since this redacted event has fields we leave the default `empty` method // that will error if called. impl RedactedEventContent for RedactedRoomAliasesEventContent { fn has_serialize_fields(&self) -> bool { self.aliases.is_some() } fn has_deserialize_fields() -> HasDeserializeFields { HasDeserializeFields::Optional } } ruma-common-0.10.5/src/events/room/avatar.rs000064400000000000000000000051711046102023000170400ustar 00000000000000//! Types for the [`m.room.avatar`] event. //! //! [`m.room.avatar`]: https://spec.matrix.org/v1.2/client-server-api/#mroomavatar use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::ThumbnailInfo; use crate::{events::EmptyStateKey, OwnedMxcUri}; /// The content of an `m.room.avatar` event. /// /// A picture that is associated with the room. /// /// This can be displayed alongside the room information. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.avatar", kind = State, state_key_type = EmptyStateKey)] pub struct RoomAvatarEventContent { /// Information about the avatar image. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// URL of the avatar image. pub url: Option, } impl RoomAvatarEventContent { /// Create an empty `RoomAvatarEventContent`. pub fn new() -> Self { Self::default() } } /// Metadata about an image (specific to avatars). #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ImageInfo { /// The height of the image in pixels. #[serde(rename = "h", skip_serializing_if = "Option::is_none")] pub height: Option, /// The width of the image in pixels. #[serde(rename = "w", skip_serializing_if = "Option::is_none")] pub width: Option, /// The MIME type of the image, e.g. "image/png." #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The file size of the image in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, /// Metadata about the image referred to in `thumbnail_url`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option>, /// The URL to the thumbnail of the image. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_url: Option, /// The [BlurHash](https://blurha.sh) for this image. /// /// This uses the unstable prefix in /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). #[cfg(feature = "unstable-msc2448")] #[serde( rename = "xyz.amorgan.blurhash", alias = "blurhash", skip_serializing_if = "Option::is_none" )] pub blurhash: Option, } impl ImageInfo { /// Create a new `ImageInfo` with all fields set to `None`. pub fn new() -> Self { Self::default() } } ruma-common-0.10.5/src/events/room/canonical_alias.rs000064400000000000000000000125041046102023000206600ustar 00000000000000//! Types for the [`m.room.canonical_alias`] event. //! //! [`m.room.canonical_alias`]: https://spec.matrix.org/v1.2/client-server-api/#mroomcanonical_alias use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{events::EmptyStateKey, OwnedRoomAliasId}; /// The content of an `m.room.canonical_alias` event. /// /// Informs the room as to which alias is the canonical one. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.canonical_alias", kind = State, state_key_type = EmptyStateKey)] pub struct RoomCanonicalAliasEventContent { /// The canonical alias. /// /// Rooms with `alias: None` should be treated the same as a room /// with no canonical alias. #[serde( default, deserialize_with = "crate::serde::empty_string_as_none", skip_serializing_if = "Option::is_none" )] pub alias: Option, /// List of alternative aliases to the room. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub alt_aliases: Vec, } impl RoomCanonicalAliasEventContent { /// Creates an empty `RoomCanonicalAliasEventContent`. pub fn new() -> Self { Self { alias: None, alt_aliases: Vec::new() } } } #[cfg(test)] mod tests { use js_int::uint; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::RoomCanonicalAliasEventContent; use crate::{ event_id, events::{EmptyStateKey, OriginalStateEvent, StateUnsigned}, room_alias_id, room_id, user_id, MilliSecondsSinceUnixEpoch, }; #[test] fn serialization_with_optional_fields_as_none() { let canonical_alias_event = OriginalStateEvent { content: RoomCanonicalAliasEventContent { alias: Some(room_alias_id!("#somewhere:localhost").to_owned()), alt_aliases: Vec::new(), }, event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!dummy:example.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), state_key: EmptyStateKey, unsigned: StateUnsigned::default(), }; let actual = to_json_value(&canonical_alias_event).unwrap(); let expected = json!({ "content": { "alias": "#somewhere:localhost" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!dummy:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.canonical_alias" }); assert_eq!(actual, expected); } #[test] fn absent_field_as_none() { let json_data = json!({ "content": {}, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!dummy:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.canonical_alias" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .alias, None ); } #[test] fn null_field_as_none() { let json_data = json!({ "content": { "alias": null }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!dummy:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.canonical_alias" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .alias, None ); } #[test] fn empty_field_as_none() { let json_data = json!({ "content": { "alias": "" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!dummy:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.canonical_alias" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .alias, None ); } #[test] fn nonempty_field_as_some() { let alias = Some(room_alias_id!("#somewhere:localhost").to_owned()); let json_data = json!({ "content": { "alias": "#somewhere:localhost" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!dummy:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.canonical_alias" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .alias, alias ); } } ruma-common-0.10.5/src/events/room/create.rs000064400000000000000000000120711046102023000170220ustar 00000000000000//! Types for the [`m.room.create`] event. //! //! [`m.room.create`]: https://spec.matrix.org/v1.2/client-server-api/#mroomcreate use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{ events::EmptyStateKey, room::RoomType, OwnedEventId, OwnedRoomId, OwnedUserId, RoomVersionId, }; /// The content of an `m.room.create` event. /// /// This is the first event in a room and cannot be changed. /// /// It acts as the root of all other events. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey)] pub struct RoomCreateEventContent { /// The `user_id` of the room creator. /// /// This is set by the homeserver. #[ruma_event(skip_redaction)] pub creator: OwnedUserId, /// Whether or not this room's data should be transferred to other homeservers. #[serde( rename = "m.federate", default = "crate::serde::default_true", skip_serializing_if = "crate::serde::is_true" )] pub federate: bool, /// The version of the room. /// /// Defaults to `RoomVersionId::V1`. #[serde(default = "default_room_version_id")] pub room_version: RoomVersionId, /// A reference to the room this room replaces, if the previous room was upgraded. #[serde(skip_serializing_if = "Option::is_none")] pub predecessor: Option, /// The room type. /// /// This is currently only used for spaces. #[serde(skip_serializing_if = "Option::is_none", rename = "type")] pub room_type: Option, } impl RoomCreateEventContent { /// Creates a new `RoomCreateEventContent` with the given creator. pub fn new(creator: OwnedUserId) -> Self { Self { creator, federate: true, room_version: default_room_version_id(), predecessor: None, room_type: None, } } } /// A reference to an old room replaced during a room version upgrade. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PreviousRoom { /// The ID of the old room. pub room_id: OwnedRoomId, /// The event ID of the last known event in the old room. pub event_id: OwnedEventId, } impl PreviousRoom { /// Creates a new `PreviousRoom` from the given room and event IDs. pub fn new(room_id: OwnedRoomId, event_id: OwnedEventId) -> Self { Self { room_id, event_id } } } /// Used to default the `room_version` field to room version 1. fn default_room_version_id() -> RoomVersionId { RoomVersionId::V1 } #[cfg(test)] mod tests { use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{RoomCreateEventContent, RoomType}; use crate::{user_id, RoomVersionId}; #[test] fn serialization() { let content = RoomCreateEventContent { creator: user_id!("@carl:example.com").to_owned(), federate: false, room_version: RoomVersionId::V4, predecessor: None, room_type: None, }; let json = json!({ "creator": "@carl:example.com", "m.federate": false, "room_version": "4" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn space_serialization() { let content = RoomCreateEventContent { creator: user_id!("@carl:example.com").to_owned(), federate: false, room_version: RoomVersionId::V4, predecessor: None, room_type: Some(RoomType::Space), }; let json = json!({ "creator": "@carl:example.com", "m.federate": false, "room_version": "4", "type": "m.space" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn deserialization() { let json = json!({ "creator": "@carl:example.com", "m.federate": true, "room_version": "4" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.creator, "@carl:example.com"); assert!(content.federate); assert_eq!(content.room_version, RoomVersionId::V4); assert_matches!(content.predecessor, None); assert_eq!(content.room_type, None); } #[test] fn space_deserialization() { let json = json!({ "creator": "@carl:example.com", "m.federate": true, "room_version": "4", "type": "m.space" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.creator, "@carl:example.com"); assert!(content.federate); assert_eq!(content.room_version, RoomVersionId::V4); assert_matches!(content.predecessor, None); assert_eq!(content.room_type, Some(RoomType::Space)); } } ruma-common-0.10.5/src/events/room/encrypted/relation_serde.rs000064400000000000000000000154341046102023000225610ustar 00000000000000use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "unstable-msc2677")] use super::Annotation; #[cfg(feature = "unstable-msc2676")] use super::Replacement; #[cfg(feature = "unstable-msc3440")] use super::Thread; use super::{InReplyTo, Reference, Relation}; #[cfg(feature = "unstable-msc3440")] use crate::OwnedEventId; impl<'de> Deserialize<'de> for Relation { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let ev = EventWithRelatesToJsonRepr::deserialize(deserializer)?; #[cfg(feature = "unstable-msc3440")] if let Some( RelationJsonRepr::ThreadStable(ThreadStableJsonRepr { event_id, is_falling_back }) | RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr { event_id, is_falling_back }), ) = ev.relates_to.relation { let in_reply_to = ev .relates_to .in_reply_to .ok_or_else(|| serde::de::Error::missing_field("m.in_reply_to"))?; return Ok(Relation::Thread(Thread { event_id, in_reply_to, is_falling_back })); } let rel = if let Some(in_reply_to) = ev.relates_to.in_reply_to { Relation::Reply { in_reply_to } } else if let Some(relation) = ev.relates_to.relation { match relation { #[cfg(feature = "unstable-msc2677")] RelationJsonRepr::Annotation(a) => Relation::Annotation(a), RelationJsonRepr::Reference(r) => Relation::Reference(r), #[cfg(feature = "unstable-msc2676")] RelationJsonRepr::Replacement(Replacement { event_id }) => { Relation::Replacement(Replacement { event_id }) } #[cfg(feature = "unstable-msc3440")] RelationJsonRepr::ThreadStable(_) | RelationJsonRepr::ThreadUnstable(_) => { unreachable!() } // FIXME: Maybe we should log this, though at this point we don't even have // access to the rel_type of the unknown relation. RelationJsonRepr::Unknown => Relation::_Custom, } } else { Relation::_Custom }; Ok(rel) } } impl Serialize for Relation { fn serialize(&self, serializer: S) -> Result where S: Serializer, { #[allow(clippy::needless_update)] let relates_to = match self { #[cfg(feature = "unstable-msc2677")] Relation::Annotation(r) => RelatesToJsonRepr { relation: Some(RelationJsonRepr::Annotation(r.clone())), ..Default::default() }, Relation::Reference(r) => RelatesToJsonRepr { relation: Some(RelationJsonRepr::Reference(r.clone())), ..Default::default() }, #[cfg(feature = "unstable-msc2676")] Relation::Replacement(r) => RelatesToJsonRepr { relation: Some(RelationJsonRepr::Replacement(r.clone())), ..Default::default() }, Relation::Reply { in_reply_to } => { RelatesToJsonRepr { in_reply_to: Some(in_reply_to.clone()), ..Default::default() } } #[cfg(feature = "unstable-msc3440")] Relation::Thread(Thread { event_id, in_reply_to, is_falling_back }) => { RelatesToJsonRepr { in_reply_to: Some(in_reply_to.clone()), relation: Some(RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr { event_id: event_id.clone(), is_falling_back: *is_falling_back, })), ..Default::default() } } Relation::_Custom => RelatesToJsonRepr::default(), }; EventWithRelatesToJsonRepr { relates_to }.serialize(serializer) } } #[derive(Deserialize, Serialize)] struct EventWithRelatesToJsonRepr { #[serde(rename = "m.relates_to", default, skip_serializing_if = "RelatesToJsonRepr::is_empty")] relates_to: RelatesToJsonRepr, } /// Struct modeling the different ways relationships can be expressed in a `m.relates_to` field of /// an event. #[derive(Default, Deserialize, Serialize)] struct RelatesToJsonRepr { #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")] in_reply_to: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] relation: Option, } impl RelatesToJsonRepr { fn is_empty(&self) -> bool { self.in_reply_to.is_none() && self.relation.is_none() } } /// A thread relation without the reply fallback, with stable names. #[derive(Clone, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] struct ThreadStableJsonRepr { /// The ID of the root message in the thread. pub event_id: OwnedEventId, /// Whether the `m.in_reply_to` field is a fallback for older clients or a real reply in a /// thread. #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] pub is_falling_back: bool, } /// A thread relation without the reply fallback, with unstable names. #[derive(Clone, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] struct ThreadUnstableJsonRepr { /// The ID of the root message in the thread. pub event_id: OwnedEventId, /// Whether the `m.in_reply_to` field is a fallback for older clients or a real reply in a /// thread. #[serde( rename = "io.element.show_reply", default, skip_serializing_if = "ruma_common::serde::is_default" )] pub is_falling_back: bool, } /// A relation, which associates new information to an existing event. #[derive(Clone, Deserialize, Serialize)] #[serde(tag = "rel_type")] enum RelationJsonRepr { /// An annotation to an event. #[cfg(feature = "unstable-msc2677")] #[serde(rename = "m.annotation")] Annotation(Annotation), /// A reference to another event. #[serde(rename = "m.reference")] Reference(Reference), /// An event that replaces another event. #[cfg(feature = "unstable-msc2676")] #[serde(rename = "m.replace")] Replacement(Replacement), /// An event that belongs to a thread, with stable names. #[cfg(feature = "unstable-msc3440")] #[serde(rename = "m.thread")] ThreadStable(ThreadStableJsonRepr), /// An event that belongs to a thread, with unstable names. #[cfg(feature = "unstable-msc3440")] #[serde(rename = "io.element.thread")] ThreadUnstable(ThreadUnstableJsonRepr), /// An unknown relation type. /// /// Not available in the public API, but exists here so deserialization /// doesn't fail with new / custom `rel_type`s. #[serde(other)] Unknown, } ruma-common-0.10.5/src/events/room/encrypted.rs000064400000000000000000000341141046102023000175560ustar 00000000000000//! Types for the [`m.room.encrypted`] event. //! //! [`m.room.encrypted`]: https://spec.matrix.org/v1.2/client-server-api/#mroomencrypted use std::collections::BTreeMap; use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::message::{self, InReplyTo}; use crate::{OwnedDeviceId, OwnedEventId}; mod relation_serde; /// The content of an `m.room.encrypted` event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.encrypted", kind = MessageLike, kind = ToDevice)] pub struct RoomEncryptedEventContent { /// Algorithm-specific fields. #[serde(flatten)] pub scheme: EncryptedEventScheme, /// Information about related events. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl RoomEncryptedEventContent { /// Creates a new `RoomEncryptedEventContent` with the given scheme and relation. pub fn new(scheme: EncryptedEventScheme, relates_to: Option) -> Self { Self { scheme, relates_to } } } impl From for RoomEncryptedEventContent { fn from(scheme: EncryptedEventScheme) -> Self { Self { scheme, relates_to: None } } } /// The to-device content of an `m.room.encrypted` event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.encrypted", kind = ToDevice)] pub struct ToDeviceRoomEncryptedEventContent { /// Algorithm-specific fields. #[serde(flatten)] pub scheme: EncryptedEventScheme, } impl ToDeviceRoomEncryptedEventContent { /// Creates a new `ToDeviceRoomEncryptedEventContent` with the given scheme. pub fn new(scheme: EncryptedEventScheme) -> Self { Self { scheme } } } impl From for ToDeviceRoomEncryptedEventContent { fn from(scheme: EncryptedEventScheme) -> Self { Self { scheme } } } /// The encryption scheme for `RoomEncryptedEventContent`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "algorithm")] pub enum EncryptedEventScheme { /// An event encrypted with `m.olm.v1.curve25519-aes-sha2`. #[serde(rename = "m.olm.v1.curve25519-aes-sha2")] OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content), /// An event encrypted with `m.megolm.v1.aes-sha2`. #[serde(rename = "m.megolm.v1.aes-sha2")] MegolmV1AesSha2(MegolmV1AesSha2Content), } /// Relationship information about an encrypted event. /// /// Outside of the encrypted payload to support server aggregation. #[derive(Clone, Debug)] #[allow(clippy::manual_non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum Relation { /// An `m.in_reply_to` relation indicating that the event is a reply to another event. Reply { /// Information about another message being replied to. in_reply_to: InReplyTo, }, /// An event that replaces another event. #[cfg(feature = "unstable-msc2676")] Replacement(Replacement), /// A reference to another event. Reference(Reference), /// An annotation to an event. #[cfg(feature = "unstable-msc2677")] Annotation(Annotation), /// An event that belongs to a thread. #[cfg(feature = "unstable-msc3440")] Thread(Thread), #[doc(hidden)] _Custom, } impl From for Relation { fn from(rel: message::Relation) -> Self { match rel { message::Relation::Reply { in_reply_to } => Self::Reply { in_reply_to }, #[cfg(feature = "unstable-msc2676")] message::Relation::Replacement(re) => { Self::Replacement(Replacement { event_id: re.event_id }) } #[cfg(feature = "unstable-msc3440")] message::Relation::Thread(t) => Self::Thread(Thread { event_id: t.event_id, in_reply_to: t.in_reply_to, is_falling_back: t.is_falling_back, }), message::Relation::_Custom => Self::_Custom, } } } /// The event this relation belongs to replaces another event. /// /// In contrast to [`message::Replacement`](super::message::Replacement), this struct doesn't /// store the new content, since that is part of the encrypted content of an `m.room.encrypted` /// events. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc2676")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Replacement { /// The ID of the event being replacing. pub event_id: OwnedEventId, } /// A reference to another event. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Reference { /// The event we are referencing. pub event_id: OwnedEventId, } impl Reference { /// Creates a new `Reference` with the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } /// An annotation for an event. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc2677")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Annotation { /// The event that is being annotated. pub event_id: OwnedEventId, /// The annotation. pub key: String, } #[cfg(feature = "unstable-msc2677")] impl Annotation { /// Creates a new `Annotation` with the given event ID and key. pub fn new(event_id: OwnedEventId, key: String) -> Self { Self { event_id, key } } } /// A thread relation for an event. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Thread { /// The ID of the root message in the thread. pub event_id: OwnedEventId, /// A reply relation. /// /// If this event is a reply and belongs to a thread, this points to the message that is being /// replied to, and `is_falling_back` must be set to `false`. /// /// If this event is not a reply, this is used as a fallback mechanism for clients that do not /// support threads. This should point to the latest message-like event in the thread and /// `is_falling_back` must be set to `true`. pub in_reply_to: InReplyTo, /// Whether the `m.in_reply_to` field is a fallback for older clients or a real reply in a /// thread. pub is_falling_back: bool, } #[cfg(feature = "unstable-msc3440")] impl Thread { /// Convenience method to create a regular `Thread` with the given event ID and latest /// message-like event ID. pub fn plain(event_id: OwnedEventId, latest_event_id: OwnedEventId) -> Self { Self { event_id, in_reply_to: InReplyTo::new(latest_event_id), is_falling_back: false } } /// Convenience method to create a reply `Thread` with the given event ID and replied-to event /// ID. pub fn reply(event_id: OwnedEventId, reply_to_event_id: OwnedEventId) -> Self { Self { event_id, in_reply_to: InReplyTo::new(reply_to_event_id), is_falling_back: true } } } /// The content of an `m.room.encrypted` event using the `m.olm.v1.curve25519-aes-sha2` algorithm. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct OlmV1Curve25519AesSha2Content { /// A map from the recipient Curve25519 identity key to ciphertext information. pub ciphertext: BTreeMap, /// The Curve25519 key of the sender. pub sender_key: String, } impl OlmV1Curve25519AesSha2Content { /// Creates a new `OlmV1Curve25519AesSha2Content` with the given ciphertext and sender key. pub fn new(ciphertext: BTreeMap, sender_key: String) -> Self { Self { ciphertext, sender_key } } } /// Ciphertext information holding the ciphertext and message type. /// /// Used for messages encrypted with the `m.olm.v1.curve25519-aes-sha2` algorithm. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct CiphertextInfo { /// The encrypted payload. pub body: String, /// The Olm message type. #[serde(rename = "type")] pub message_type: UInt, } impl CiphertextInfo { /// Creates a new `CiphertextInfo` with the given body and type. pub fn new(body: String, message_type: UInt) -> Self { Self { body, message_type } } } /// The content of an `m.room.encrypted` event using the `m.megolm.v1.aes-sha2` algorithm. /// /// To create an instance of this type, first create a `MegolmV1AesSha2ContentInit` and convert it /// via `MegolmV1AesSha2Content::from` / `.into()`. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct MegolmV1AesSha2Content { /// The encrypted content of the event. pub ciphertext: String, /// The Curve25519 key of the sender. #[deprecated = "this field still needs to be sent but should not be used when received"] pub sender_key: String, /// The ID of the sending device. #[deprecated = "this field still needs to be sent but should not be used when received"] pub device_id: OwnedDeviceId, /// The ID of the session used to encrypt the message. pub session_id: String, } /// Mandatory initial set of fields of `MegolmV1AesSha2Content`. /// /// This struct will not be updated even if additional fields are added to `MegolmV1AesSha2Content` /// in a new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct MegolmV1AesSha2ContentInit { /// The encrypted content of the event. pub ciphertext: String, /// The Curve25519 key of the sender. pub sender_key: String, /// The ID of the sending device. pub device_id: OwnedDeviceId, /// The ID of the session used to encrypt the message. pub session_id: String, } impl From for MegolmV1AesSha2Content { /// Creates a new `MegolmV1AesSha2Content` from the given init struct. fn from(init: MegolmV1AesSha2ContentInit) -> Self { let MegolmV1AesSha2ContentInit { ciphertext, sender_key, device_id, session_id } = init; #[allow(deprecated)] Self { ciphertext, sender_key, device_id, session_id } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use js_int::uint; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{ EncryptedEventScheme, InReplyTo, MegolmV1AesSha2ContentInit, Relation, RoomEncryptedEventContent, }; use crate::{event_id, serde::Raw}; #[test] fn serialization() { let key_verification_start_content = RoomEncryptedEventContent { scheme: EncryptedEventScheme::MegolmV1AesSha2( MegolmV1AesSha2ContentInit { ciphertext: "ciphertext".into(), sender_key: "sender_key".into(), device_id: "device_id".into(), session_id: "session_id".into(), } .into(), ), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: event_id!("$h29iv0s8:example.com").to_owned() }, }), }; let json_data = json!({ "algorithm": "m.megolm.v1.aes-sha2", "ciphertext": "ciphertext", "sender_key": "sender_key", "device_id": "device_id", "session_id": "session_id", "m.relates_to": { "m.in_reply_to": { "event_id": "$h29iv0s8:example.com" } }, }); assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data); } #[test] #[allow(deprecated)] fn deserialization() { let json_data = json!({ "algorithm": "m.megolm.v1.aes-sha2", "ciphertext": "ciphertext", "sender_key": "sender_key", "device_id": "device_id", "session_id": "session_id", "m.relates_to": { "m.in_reply_to": { "event_id": "$h29iv0s8:example.com" } }, }); let content: RoomEncryptedEventContent = from_json_value(json_data).unwrap(); let scheme = assert_matches!( content.scheme, EncryptedEventScheme::MegolmV1AesSha2(scheme) => scheme ); assert_eq!(scheme.ciphertext, "ciphertext"); assert_eq!(scheme.sender_key, "sender_key"); assert_eq!(scheme.device_id, "device_id"); assert_eq!(scheme.session_id, "session_id"); let in_reply_to = assert_matches!( content.relates_to, Some(Relation::Reply { in_reply_to }) => in_reply_to ); assert_eq!(in_reply_to.event_id, "$h29iv0s8:example.com"); } #[test] fn deserialization_olm() { let json_data = json!({ "sender_key": "test_key", "ciphertext": { "test_curve_key": { "body": "encrypted_body", "type": 1 } }, "algorithm": "m.olm.v1.curve25519-aes-sha2" }); let content: RoomEncryptedEventContent = from_json_value(json_data).unwrap(); let c = assert_matches!( content.scheme, EncryptedEventScheme::OlmV1Curve25519AesSha2(c) => c ); assert_eq!(c.sender_key, "test_key"); assert_eq!(c.ciphertext.len(), 1); assert_eq!(c.ciphertext["test_curve_key"].body, "encrypted_body"); assert_eq!(c.ciphertext["test_curve_key"].message_type, uint!(1)); } #[test] fn deserialization_failure() { from_json_value::>( json!({ "algorithm": "m.megolm.v1.aes-sha2" }), ) .unwrap() .deserialize() .unwrap_err(); } } ruma-common-0.10.5/src/events/room/encryption.rs000064400000000000000000000030441046102023000177510ustar 00000000000000//! Types for the [`m.room.encryption`] event. //! //! [`m.room.encryption`]: https://spec.matrix.org/v1.2/client-server-api/#mroomencryption use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::events::{EmptyStateKey, EventEncryptionAlgorithm}; /// The content of an `m.room.encryption` event. /// /// Defines how messages sent in this room should be encrypted. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.encryption", kind = State, state_key_type = EmptyStateKey)] pub struct RoomEncryptionEventContent { /// The encryption algorithm to be used to encrypt messages sent in this room. /// /// Must be `m.megolm.v1.aes-sha2`. pub algorithm: EventEncryptionAlgorithm, /// How long the session should be used before changing it. /// /// `uint!(604800000)` (a week) is the recommended default. #[serde(skip_serializing_if = "Option::is_none")] pub rotation_period_ms: Option, /// How many messages should be sent before changing the session. /// /// `uint!(100)` is the recommended default. #[serde(skip_serializing_if = "Option::is_none")] pub rotation_period_msgs: Option, } impl RoomEncryptionEventContent { /// Creates a new `RoomEncryptionEventContent` with the given algorithm. pub fn new(algorithm: EventEncryptionAlgorithm) -> Self { Self { algorithm, rotation_period_ms: None, rotation_period_msgs: None } } } ruma-common-0.10.5/src/events/room/guest_access.rs000064400000000000000000000042621046102023000202320ustar 00000000000000//! Types for the [`m.room.guest_access`] event. //! //! [`m.room.guest_access`]: https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{events::EmptyStateKey, serde::StringEnum, PrivOwnedStr}; /// The content of an `m.room.guest_access` event. /// /// Controls whether guest users are allowed to join rooms. /// /// This event controls whether guest users are allowed to join rooms. If this event is absent, /// servers should act as if it is present and has the value `GuestAccess::Forbidden`. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.guest_access", kind = State, state_key_type = EmptyStateKey)] pub struct RoomGuestAccessEventContent { /// A policy for guest user access to a room. pub guest_access: GuestAccess, } impl RoomGuestAccessEventContent { /// Creates a new `RoomGuestAccessEventContent` with the given policy. pub fn new(guest_access: GuestAccess) -> Self { Self { guest_access } } } impl RoomGuestAccessEvent { /// Obtain the guest access policy, regardless of whether this event is redacted. pub fn guest_access(&self) -> &GuestAccess { match self { Self::Original(ev) => &ev.content.guest_access, Self::Redacted(_) => &GuestAccess::Forbidden, } } } impl SyncRoomGuestAccessEvent { /// Obtain the guest access policy, regardless of whether this event is redacted. pub fn guest_access(&self) -> &GuestAccess { match self { Self::Original(ev) => &ev.content.guest_access, Self::Redacted(_) => &GuestAccess::Forbidden, } } } /// A policy for guest user access to a room. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum GuestAccess { /// Guests are allowed to join the room. CanJoin, /// Guests are not allowed to join the room. Forbidden, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/events/room/history_visibility.rs000064400000000000000000000057371046102023000215420ustar 00000000000000//! Types for the [`m.room.history_visibility`] event. //! //! [`m.room.history_visibility`]: https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{events::EmptyStateKey, serde::StringEnum, PrivOwnedStr}; /// The content of an `m.room.history_visibility` event. /// /// This event controls whether a member of a room can see the events that happened in a room from /// before they joined. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.history_visibility", kind = State, state_key_type = EmptyStateKey)] pub struct RoomHistoryVisibilityEventContent { /// Who can see the room history. #[ruma_event(skip_redaction)] pub history_visibility: HistoryVisibility, } impl RoomHistoryVisibilityEventContent { /// Creates a new `RoomHistoryVisibilityEventContent` with the given policy. pub fn new(history_visibility: HistoryVisibility) -> Self { Self { history_visibility } } } impl RoomHistoryVisibilityEvent { /// Obtain the history visibility, regardless of whether this event is redacted. pub fn history_visibility(&self) -> &HistoryVisibility { match self { Self::Original(ev) => &ev.content.history_visibility, Self::Redacted(ev) => &ev.content.history_visibility, } } } impl SyncRoomHistoryVisibilityEvent { /// Obtain the history visibility, regardless of whether this event is redacted. pub fn history_visibility(&self) -> &HistoryVisibility { match self { Self::Original(ev) => &ev.content.history_visibility, Self::Redacted(ev) => &ev.content.history_visibility, } } } /// Who can see a room's history. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum HistoryVisibility { /// Previous events are accessible to newly joined members from the point they were invited /// onwards. /// /// Events stop being accessible when the member's state changes to something other than /// *invite* or *join*. Invited, /// Previous events are accessible to newly joined members from the point they joined the room /// onwards. /// Events stop being accessible when the member's state changes to something other than /// *join*. Joined, /// Previous events are always accessible to newly joined members. /// /// All events in the room are accessible, even those sent when the member was not a part of /// the room. Shared, /// All events while this is the `HistoryVisibility` value may be shared by any participating /// homeserver with anyone, regardless of whether they have ever joined the room. WorldReadable, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/events/room/join_rules.rs000064400000000000000000000245221046102023000177340ustar 00000000000000//! Types for the [`m.room.join_rules`] event. //! //! [`m.room.join_rules`]: https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules use std::{borrow::Cow, collections::BTreeMap}; use ruma_macros::EventContent; use serde::{ de::{Deserializer, Error}, Deserialize, Serialize, }; use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; use crate::{events::EmptyStateKey, serde::from_raw_json_value, OwnedRoomId, PrivOwnedStr}; /// The content of an `m.room.join_rules` event. /// /// Describes how users are allowed to join the room. #[derive(Clone, Debug, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.join_rules", kind = State, state_key_type = EmptyStateKey)] pub struct RoomJoinRulesEventContent { /// The type of rules used for users wishing to join this room. #[ruma_event(skip_redaction)] #[serde(flatten)] pub join_rule: JoinRule, } impl RoomJoinRulesEventContent { /// Creates a new `RoomJoinRulesEventContent` with the given rule. pub fn new(join_rule: JoinRule) -> Self { Self { join_rule } } /// Creates a new `RoomJoinRulesEventContent` with the restricted rule and the given set of /// allow rules. pub fn restricted(allow: Vec) -> Self { Self { join_rule: JoinRule::Restricted(Restricted::new(allow)) } } /// Creates a new `RoomJoinRulesEventContent` with the knock restricted rule and the given set /// of allow rules. pub fn knock_restricted(allow: Vec) -> Self { Self { join_rule: JoinRule::KnockRestricted(Restricted::new(allow)) } } } impl<'de> Deserialize<'de> for RoomJoinRulesEventContent { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let join_rule = JoinRule::deserialize(deserializer)?; Ok(RoomJoinRulesEventContent { join_rule }) } } impl RoomJoinRulesEvent { /// Obtain the join rule, regardless of whether this event is redacted. pub fn join_rule(&self) -> &JoinRule { match self { Self::Original(ev) => &ev.content.join_rule, Self::Redacted(ev) => &ev.content.join_rule, } } } impl SyncRoomJoinRulesEvent { /// Obtain the join rule, regardless of whether this event is redacted. pub fn join_rule(&self) -> &JoinRule { match self { Self::Original(ev) => &ev.content.join_rule, Self::Redacted(ev) => &ev.content.join_rule, } } } /// The rule used for users wishing to join this room. /// /// This type can hold an arbitrary string. To check for values that are not available as a /// documented variant here, use its string representation, obtained through `.as_str()`. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "join_rule", rename_all = "snake_case")] pub enum JoinRule { /// A user who wishes to join the room must first receive an invite to the room from someone /// already inside of the room. Invite, /// Users can join the room if they are invited, or they can request an invite to the room. /// /// They can be allowed (invited) or denied (kicked/banned) access. Knock, /// Reserved but not yet implemented by the Matrix specification. Private, /// Users can join the room if they are invited, or if they meet any of the conditions /// described in a set of [`AllowRule`]s. Restricted(Restricted), /// Users can join the room if they are invited, or if they meet any of the conditions /// described in a set of [`AllowRule`]s, or they can request an invite to the room. KnockRestricted(Restricted), /// Anyone can join the room without any prior action. Public, #[doc(hidden)] #[serde(skip_serializing)] _Custom(PrivOwnedStr), } impl JoinRule { /// Returns the string name of this `JoinRule` pub fn as_str(&self) -> &str { match self { JoinRule::Invite => "invite", JoinRule::Knock => "knock", JoinRule::Private => "private", JoinRule::Restricted(_) => "restricted", JoinRule::KnockRestricted(_) => "knock_restricted", JoinRule::Public => "public", JoinRule::_Custom(rule) => &rule.0, } } } impl<'de> Deserialize<'de> for JoinRule { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let json: Box = Box::deserialize(deserializer)?; #[derive(Deserialize)] struct ExtractType<'a> { #[serde(borrow)] join_rule: Option>, } let join_rule = serde_json::from_str::>(json.get()) .map_err(serde::de::Error::custom)? .join_rule .ok_or_else(|| D::Error::missing_field("join_rule"))?; match join_rule.as_ref() { "invite" => Ok(Self::Invite), "knock" => Ok(Self::Knock), "private" => Ok(Self::Private), "restricted" => from_raw_json_value(&json).map(Self::Restricted), "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted), "public" => Ok(Self::Public), _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))), } } } /// Configuration of the `Restricted` join rule. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Restricted { /// Allow rules which describe conditions that allow joining a room. pub allow: Vec, } impl Restricted { /// Constructs a new rule set for restricted rooms with the given rules. pub fn new(allow: Vec) -> Self { Self { allow } } } /// An allow rule which defines a condition that allows joining a room. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "type")] pub enum AllowRule { /// Joining is allowed if a user is already a member of the room with the id `room_id`. #[serde(rename = "m.room_membership")] RoomMembership(RoomMembership), #[doc(hidden)] _Custom(CustomAllowRule), } impl AllowRule { /// Constructs an `AllowRule` with membership of the room with the given id as its predicate. pub fn room_membership(room_id: OwnedRoomId) -> Self { Self::RoomMembership(RoomMembership::new(room_id)) } } /// Allow rule which grants permission to join based on the membership of another room. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RoomMembership { /// The id of the room which being a member of grants permission to join another room. pub room_id: OwnedRoomId, } impl RoomMembership { /// Constructs a new room membership rule for the given room id. pub fn new(room_id: OwnedRoomId) -> Self { Self { room_id } } } #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct CustomAllowRule { #[serde(rename = "type")] rule_type: String, #[serde(flatten)] extra: BTreeMap, } impl<'de> Deserialize<'de> for AllowRule { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let json: Box = Box::deserialize(deserializer)?; // Extracts the `type` value. #[derive(Deserialize)] struct ExtractType<'a> { #[serde(borrow, rename = "type")] rule_type: Option>, } // Get the value of `type` if present. let rule_type = serde_json::from_str::>(json.get()) .map_err(serde::de::Error::custom)? .rule_type; match rule_type.as_deref() { Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership), Some(_) => from_raw_json_value(&json).map(Self::_Custom), None => Err(D::Error::missing_field("type")), } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::{AllowRule, JoinRule, OriginalSyncRoomJoinRulesEvent, RoomJoinRulesEventContent}; use crate::room_id; #[test] fn deserialize() { let json = r#"{"join_rule": "public"}"#; let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap(); assert_matches!(event, RoomJoinRulesEventContent { join_rule: JoinRule::Public }); } #[test] fn deserialize_restricted() { let json = r#"{ "join_rule": "restricted", "allow": [ { "type": "m.room_membership", "room_id": "!mods:example.org" }, { "type": "m.room_membership", "room_id": "!users:example.org" } ] }"#; let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap(); match event.join_rule { JoinRule::Restricted(restricted) => assert_eq!( restricted.allow, &[ AllowRule::room_membership(room_id!("!mods:example.org").to_owned()), AllowRule::room_membership(room_id!("!users:example.org").to_owned()) ] ), rule => panic!("Deserialized to wrong variant: {rule:?}"), } } #[test] fn deserialize_restricted_event() { let json = r#"{ "type": "m.room.join_rules", "sender": "@admin:community.rs", "content": { "join_rule": "restricted", "allow":[ { "type": "m.room_membership","room_id": "!KqeUnzmXPIhHRaWMTs:mccarty.io" } ] }, "state_key": "", "origin_server_ts":1630508835342, "unsigned": { "age":4165521871 }, "event_id": "$0ACb9KSPlT3al3kikyRYvFhMqXPP9ZcQOBrsdIuh58U" }"#; assert_matches!(serde_json::from_str::(json), Ok(_)); } } ruma-common-0.10.5/src/events/room/member/change.rs000064400000000000000000000102211046102023000202460ustar 00000000000000use super::MembershipState; use crate::{MxcUri, UserId}; /// The details of a member event required to calculate a [`MembershipChange`]. #[derive(Clone, Debug)] pub struct MembershipDetails<'a> { pub(crate) avatar_url: Option<&'a MxcUri>, pub(crate) displayname: Option<&'a str>, pub(crate) membership: &'a MembershipState, } /// Translation of the membership change in `m.room.member` event. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum MembershipChange<'a> { /// No change. None, /// Must never happen. Error, /// User joined the room. Joined, /// User left the room. Left, /// User was banned. Banned, /// User was unbanned. Unbanned, /// User was kicked. Kicked, /// User was invited. Invited, /// User was kicked and banned. KickedAndBanned, /// User rejected the invite. InvitationRejected, /// User had their invite revoked. InvitationRevoked, /// User knocked. Knocked, /// User had their knock accepted. KnockAccepted, /// User retracted their knock. KnockRetracted, /// User had their knock denied. KnockDenied, /// `displayname` or `avatar_url` changed. ProfileChanged { /// The details of the displayname change, if applicable. displayname_change: Option>>, /// The details of the avatar url change, if applicable. avatar_url_change: Option>>, }, /// Not implemented. NotImplemented, } /// A simple representation of a change, containing old and new data. #[derive(Clone, Debug)] #[allow(clippy::exhaustive_structs)] pub struct Change { /// The old data. pub old: T, /// The new data. pub new: T, } impl Change { fn new(old: T, new: T) -> Option { if old == new { None } else { Some(Self { old, new }) } } } /// Internal function so all `RoomMemberEventContent` state event kinds can share the same /// implementation. /// /// This must match the table for [`m.room.member`] in the spec. /// /// [`m.room.member`]: https://spec.matrix.org/v1.2/client-server-api/#mroommember pub(super) fn membership_change<'a>( details: MembershipDetails<'a>, prev_details: Option>, sender: &UserId, state_key: &UserId, ) -> MembershipChange<'a> { use MembershipChange as Ch; use MembershipState as St; let prev_details = match prev_details { Some(prev) => prev, None => MembershipDetails { avatar_url: None, displayname: None, membership: &St::Leave }, }; match (&prev_details.membership, &details.membership) { (St::Invite, St::Invite) | (St::Leave, St::Leave) | (St::Ban, St::Ban) | (St::Knock, St::Knock) => Ch::None, (St::Invite, St::Join) | (St::Leave, St::Join) => Ch::Joined, (St::Invite, St::Leave) if sender == state_key => Ch::InvitationRevoked, (St::Invite, St::Leave) => Ch::InvitationRejected, (St::Invite, St::Ban) | (St::Leave, St::Ban) | (St::Knock, St::Ban) => Ch::Banned, (St::Join, St::Invite) | (St::Ban, St::Invite) | (St::Ban, St::Join) | (St::Join, St::Knock) | (St::Ban, St::Knock) | (St::Knock, St::Join) => Ch::Error, (St::Join, St::Join) => Ch::ProfileChanged { displayname_change: Change::new(prev_details.displayname, details.displayname), avatar_url_change: Change::new(prev_details.avatar_url, details.avatar_url), }, (St::Join, St::Leave) if sender == state_key => Ch::Left, (St::Join, St::Leave) => Ch::Kicked, (St::Join, St::Ban) => Ch::KickedAndBanned, (St::Leave, St::Invite) => Ch::Invited, (St::Ban, St::Leave) => Ch::Unbanned, (St::Leave, St::Knock) | (St::Invite, St::Knock) => Ch::Knocked, (St::Knock, St::Invite) => Ch::KnockAccepted, (St::Knock, St::Leave) if sender == state_key => Ch::KnockRetracted, (St::Knock, St::Leave) => Ch::KnockDenied, _ => Ch::NotImplemented, } } ruma-common-0.10.5/src/events/room/member.rs000064400000000000000000000715011046102023000170310ustar 00000000000000//! Types for the [`m.room.member`] event. //! //! [`m.room.member`]: https://spec.matrix.org/v1.2/client-server-api/#mroommember use std::collections::BTreeMap; use js_int::Int; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; use crate::{ events::{ AnyStrippedStateEvent, EventContent, HasDeserializeFields, RedactContent, RedactedEventContent, RedactedStateEventContent, Relations, StateEventContent, StateEventType, StateUnsigned, StateUnsignedFromParts, StaticEventContent, }, serde::{CanBeEmpty, Raw, StringEnum}, OwnedMxcUri, OwnedServerName, OwnedServerSigningKeyId, OwnedTransactionId, OwnedUserId, PrivOwnedStr, RoomVersionId, }; mod change; use self::change::membership_change; pub use self::change::{Change, MembershipChange, MembershipDetails}; /// The content of an `m.room.member` event. /// /// The current membership state of a user in the room. /// /// Adjusts the membership state for a user in a room. It is preferable to use the membership /// APIs (`/rooms//invite` etc) when performing membership actions rather than /// adjusting the state directly as there are a restricted set of valid transformations. For /// example, user A cannot force user B to join a room, and trying to force this state change /// directly will fail. /// /// This event may also include an `invite_room_state` key inside the event's unsigned data, but /// Ruma doesn't currently expose this; see [#998](https://github.com/ruma/ruma/issues/998). /// /// The user for which a membership applies is represented by the `state_key`. Under some /// conditions, the `sender` and `state_key` may not match - this may be interpreted as the /// `sender` affecting the membership state of the `state_key` user. /// /// The membership for a given user can change over time. Previous membership can be retrieved /// from the `prev_content` object on an event. If not present, the user's previous membership /// must be assumed as leave. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event( type = "m.room.member", kind = State, state_key_type = OwnedUserId, unsigned_type = RoomMemberUnsigned, custom_redacted, )] pub struct RoomMemberEventContent { /// The avatar URL for this user, if any. /// /// This is added by the homeserver. If you activate the `compat` feature, this field being an /// empty string in JSON will result in `None` here during deserialization. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr( feature = "compat", serde(default, deserialize_with = "crate::serde::empty_string_as_none") )] pub avatar_url: Option, /// The display name for this user, if any. /// /// This is added by the homeserver. #[serde(skip_serializing_if = "Option::is_none")] pub displayname: Option, /// Flag indicating whether the room containing this event was created with the intention of /// being a direct chat. #[serde(skip_serializing_if = "Option::is_none")] pub is_direct: Option, /// The membership state of this user. pub membership: MembershipState, /// If this member event is the successor to a third party invitation, this field will /// contain information about that invitation. #[serde(skip_serializing_if = "Option::is_none")] pub third_party_invite: Option, /// The [BlurHash](https://blurha.sh) for the avatar pointed to by `avatar_url`. /// /// This uses the unstable prefix in /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). #[cfg(feature = "unstable-msc2448")] #[serde( rename = "xyz.amorgan.blurhash", alias = "blurhash", skip_serializing_if = "Option::is_none" )] pub blurhash: Option, /// User-supplied text for why their membership has changed. /// /// For kicks and bans, this is typically the reason for the kick or ban. For other membership /// changes, this is a way for the user to communicate their intent without having to send a /// message to the room, such as in a case where Bob rejects an invite from Alice about an /// upcoming concert, but can't make it that day. /// /// Clients are not recommended to show this reason to users when receiving an invite due to /// the potential for spam and abuse. Hiding the reason behind a button or other component /// is recommended. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, /// Arbitrarily chosen `UserId` (MxID) of a local user who can send an invite. #[serde(rename = "join_authorised_via_users_server")] #[serde(skip_serializing_if = "Option::is_none")] pub join_authorized_via_users_server: Option, } impl RoomMemberEventContent { /// Creates a new `RoomMemberEventContent` with the given membership state. pub fn new(membership: MembershipState) -> Self { Self { membership, avatar_url: None, displayname: None, is_direct: None, third_party_invite: None, #[cfg(feature = "unstable-msc2448")] blurhash: None, reason: None, join_authorized_via_users_server: None, } } /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { MembershipDetails { avatar_url: self.avatar_url.as_deref(), displayname: self.displayname.as_deref(), membership: &self.membership, } } } impl RedactContent for RoomMemberEventContent { type Redacted = RedactedRoomMemberEventContent; fn redact(self, _version: &RoomVersionId) -> RedactedRoomMemberEventContent { RedactedRoomMemberEventContent { membership: self.membership, join_authorized_via_users_server: match _version { RoomVersionId::V9 | RoomVersionId::V10 => self.join_authorized_via_users_server, _ => None, }, } } } /// A member event that has been redacted. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RedactedRoomMemberEventContent { /// The membership state of this user. pub membership: MembershipState, /// An arbitrary user who has the power to issue invites. /// /// This is redacted in room versions 8 and below. It is used for validating /// joins when the join rule is restricted. #[serde(rename = "join_authorised_via_users_server")] pub join_authorized_via_users_server: Option, } impl RedactedRoomMemberEventContent { /// Create a `RedactedRoomMemberEventContent` with the given membership. pub fn new(membership: MembershipState) -> Self { Self { membership, join_authorized_via_users_server: None } } /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership } } } impl EventContent for RedactedRoomMemberEventContent { type EventType = StateEventType; fn event_type(&self) -> StateEventType { StateEventType::RoomMember } fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result { if event_type != "m.room.member" { return Err(::serde::de::Error::custom(format!( "expected event type `m.room.member`, found `{}`", event_type ))); } serde_json::from_str(content.get()) } } impl StateEventContent for RedactedRoomMemberEventContent { type StateKey = OwnedUserId; // FIXME: Not actually used type Unsigned = StateUnsigned; } impl RedactedStateEventContent for RedactedRoomMemberEventContent {} // Since this redacted event has fields we leave the default `empty` method // that will error if called. impl RedactedEventContent for RedactedRoomMemberEventContent { fn has_serialize_fields(&self) -> bool { true } fn has_deserialize_fields() -> HasDeserializeFields { HasDeserializeFields::Optional } } impl RoomMemberEvent { /// Obtain the membership state, regardless of whether this event is redacted. pub fn membership(&self) -> &MembershipState { match self { Self::Original(ev) => &ev.content.membership, Self::Redacted(ev) => &ev.content.membership, } } } impl SyncRoomMemberEvent { /// Obtain the membership state, regardless of whether this event is redacted. pub fn membership(&self) -> &MembershipState { match self { Self::Original(ev) => &ev.content.membership, Self::Redacted(ev) => &ev.content.membership, } } } /// The membership state of a user. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "lowercase")] #[non_exhaustive] pub enum MembershipState { /// The user is banned. Ban, /// The user has been invited. Invite, /// The user has joined. Join, /// The user has requested to join. Knock, /// The user has left. Leave, #[doc(hidden)] _Custom(PrivOwnedStr), } /// Information about a third party invitation. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ThirdPartyInvite { /// A name which can be displayed to represent the user instead of their third party /// identifier. pub display_name: String, /// A block of content which has been signed, which servers can use to verify the event. /// /// Clients should ignore this. pub signed: SignedContent, } impl ThirdPartyInvite { /// Creates a new `ThirdPartyInvite` with the given display name and signed content. pub fn new(display_name: String, signed: SignedContent) -> Self { Self { display_name, signed } } } /// A block of content which has been signed, which servers can use to verify a third party /// invitation. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SignedContent { /// The invited Matrix user ID. /// /// Must be equal to the user_id property of the event. pub mxid: OwnedUserId, /// A single signature from the verifying server, in the format specified by the Signing Events /// section of the server-server API. pub signatures: BTreeMap>, /// The token property of the containing `third_party_invite` object. pub token: String, } impl SignedContent { /// Creates a new `SignedContent` with the given mxid, signature and token. pub fn new( signatures: BTreeMap>, mxid: OwnedUserId, token: String, ) -> Self { Self { mxid, signatures, token } } } impl OriginalRoomMemberEvent { /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { self.content.details() } /// Get a reference to the `prev_content` in unsigned, if it exists. /// /// Shorthand for `event.unsigned.prev_content.as_ref()` pub fn prev_content(&self) -> Option<&RoomMemberEventContent> { self.unsigned.prev_content.as_ref() } fn prev_details(&self) -> Option> { self.prev_content().map(|c| c.details()) } /// Helper function for membership change. /// /// Check [the specification][spec] for details. /// /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember pub fn membership_change(&self) -> MembershipChange<'_> { membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key) } } impl RedactedRoomMemberEvent { /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { self.content.details() } /// Helper function for membership change. /// /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()` /// of the previous `m.room.member` event manually (if there is a previous `m.room.member` /// event). /// /// Check [the specification][spec] for details. /// /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember pub fn membership_change<'a>( &'a self, prev_details: Option>, ) -> MembershipChange<'a> { membership_change(self.details(), prev_details, &self.sender, &self.state_key) } } impl OriginalSyncRoomMemberEvent { /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { self.content.details() } /// Get a reference to the `prev_content` in unsigned, if it exists. /// /// Shorthand for `event.unsigned.prev_content.as_ref()` pub fn prev_content(&self) -> Option<&RoomMemberEventContent> { self.unsigned.prev_content.as_ref() } fn prev_details(&self) -> Option> { self.prev_content().map(|c| c.details()) } /// Helper function for membership change. /// /// Check [the specification][spec] for details. /// /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember pub fn membership_change(&self) -> MembershipChange<'_> { membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key) } } impl RedactedSyncRoomMemberEvent { /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { self.content.details() } /// Helper function for membership change. /// /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()` /// of the previous `m.room.member` event manually (if there is a previous `m.room.member` /// event). /// /// Check [the specification][spec] for details. /// /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember pub fn membership_change<'a>( &'a self, prev_details: Option>, ) -> MembershipChange<'a> { membership_change(self.details(), prev_details, &self.sender, &self.state_key) } } impl StrippedRoomMemberEvent { /// Obtain the details about this event that are required to calculate a membership change. /// /// This is required when you want to calculate the change a redacted `m.room.member` event /// made. pub fn details(&self) -> MembershipDetails<'_> { self.content.details() } /// Helper function for membership change. /// /// Since stripped events don't have `unsigned.prev_content`, you have to pass the `.details()` /// of the previous `m.room.member` event manually (if there is a previous `m.room.member` /// event). /// /// Check [the specification][spec] for details. /// /// [spec]: https://spec.matrix.org/v1.2/client-server-api/#mroommember pub fn membership_change<'a>( &'a self, prev_details: Option>, ) -> MembershipChange<'a> { membership_change(self.details(), prev_details, &self.sender, &self.state_key) } } /// Extra information about a message event that is not incorporated into the event's hash. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RoomMemberUnsigned { /// The time in milliseconds that has elapsed since the event was sent. /// /// This field is generated by the local homeserver, and may be incorrect if the local time on /// at least one of the two servers is out of sync, which can cause the age to either be /// negative or greater than it actually is. #[serde(skip_serializing_if = "Option::is_none")] pub age: Option, /// The client-supplied transaction ID, if the client being given the event is the same one /// which sent it. #[serde(skip_serializing_if = "Option::is_none")] pub transaction_id: Option, /// Optional previous content of the event. #[serde(skip_serializing_if = "Option::is_none")] pub prev_content: Option, /// State events to assist the receiver in identifying the room. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub invite_room_state: Vec>, /// [Bundled aggregations] of related child events. /// /// [Bundled aggregations]: https://spec.matrix.org/v1.3/client-server-api/#aggregations #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")] pub relations: Option, } impl RoomMemberUnsigned { /// Create a new `Unsigned` with fields set to `None`. pub fn new() -> Self { Self::default() } } impl CanBeEmpty for RoomMemberUnsigned { /// Whether this unsigned data is empty (all fields are `None`). /// /// This method is used to determine whether to skip serializing the `unsigned` field in room /// events. Do not use it to determine whether an incoming `unsigned` field was present - it /// could still have been present but contained none of the known fields. fn is_empty(&self) -> bool { self.age.is_none() && self.transaction_id.is_none() && self.prev_content.is_none() && self.invite_room_state.is_empty() && self.relations.is_none() } } impl StateUnsignedFromParts for RoomMemberUnsigned { fn _from_parts(event_type: &str, object: &RawJsonValue) -> serde_json::Result { const EVENT_TYPE: &str = ::TYPE; if event_type != EVENT_TYPE { return Err(serde::de::Error::custom(format!( "expected event type of `{EVENT_TYPE}`, found `{event_type}`", ))); } from_json_str(object.get()) } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use js_int::uint; use maplit::btreemap; use serde_json::{from_value as from_json_value, json}; use super::{MembershipState, RoomMemberEventContent}; use crate::{ events::OriginalStateEvent, mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_id, user_id, MilliSecondsSinceUnixEpoch, }; #[test] fn serde_with_no_prev_content() { let json = json!({ "type": "m.room.member", "content": { "membership": "join" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "@carl:example.com" }); let ev = from_json_value::>(json).unwrap(); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.room_id, "!n8f893n9:example.com"); assert_eq!(ev.sender, "@carl:example.com"); assert_eq!(ev.state_key, "@carl:example.com"); assert!(ev.unsigned.is_empty()); assert_eq!(ev.content.avatar_url, None); assert_eq!(ev.content.displayname, None); assert_eq!(ev.content.is_direct, None); assert_eq!(ev.content.membership, MembershipState::Join); assert_matches!(ev.content.third_party_invite, None); } #[test] fn serde_with_prev_content() { let json = json!({ "type": "m.room.member", "content": { "membership": "join" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "@carl:example.com", "unsigned": { "prev_content": { "membership": "join" }, }, }); let ev = from_json_value::>(json).unwrap(); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.room_id, "!n8f893n9:example.com"); assert_eq!(ev.sender, "@carl:example.com"); assert_eq!(ev.state_key, "@carl:example.com"); assert_eq!(ev.content.avatar_url, None); assert_eq!(ev.content.displayname, None); assert_eq!(ev.content.is_direct, None); assert_eq!(ev.content.membership, MembershipState::Join); assert_matches!(ev.content.third_party_invite, None); let prev_content = ev.unsigned.prev_content.unwrap(); assert_eq!(prev_content.avatar_url, None); assert_eq!(prev_content.displayname, None); assert_eq!(prev_content.is_direct, None); assert_eq!(prev_content.membership, MembershipState::Join); assert_matches!(prev_content.third_party_invite, None); } #[test] fn serde_with_content_full() { let json = json!({ "type": "m.room.member", "content": { "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", "displayname": "Alice Margatroid", "is_direct": true, "membership": "invite", "third_party_invite": { "display_name": "alice", "signed": { "mxid": "@alice:example.org", "signatures": { "magic.forest": { "ed25519:3": "foobar" } }, "token": "abc123" } } }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 233, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@alice:example.org", "state_key": "@alice:example.org" }); let ev = from_json_value::>(json).unwrap(); assert_eq!(ev.event_id, "$143273582443PhrSn:example.org"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233))); assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org"); assert_eq!(ev.sender, "@alice:example.org"); assert_eq!(ev.state_key, "@alice:example.org"); assert!(ev.unsigned.is_empty()); assert_eq!( ev.content.avatar_url.as_deref(), Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF")) ); assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid")); assert_eq!(ev.content.is_direct, Some(true)); assert_eq!(ev.content.membership, MembershipState::Invite); let third_party_invite = ev.content.third_party_invite.unwrap(); assert_eq!(third_party_invite.display_name, "alice"); assert_eq!(third_party_invite.signed.mxid, "@alice:example.org"); assert_eq!( third_party_invite.signed.signatures, btreemap! { server_name!("magic.forest").to_owned() => btreemap! { server_signing_key_id!("ed25519:3").to_owned() => "foobar".to_owned() } } ); assert_eq!(third_party_invite.signed.token, "abc123"); } #[test] fn serde_with_prev_content_full() { let json = json!({ "type": "m.room.member", "content": { "membership": "join", }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 233, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@alice:example.org", "state_key": "@alice:example.org", "unsigned": { "prev_content": { "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", "displayname": "Alice Margatroid", "is_direct": true, "membership": "invite", "third_party_invite": { "display_name": "alice", "signed": { "mxid": "@alice:example.org", "signatures": { "magic.forest": { "ed25519:3": "foobar", }, }, "token": "abc123" }, }, }, }, }); let ev = from_json_value::>(json).unwrap(); assert_eq!(ev.event_id, "$143273582443PhrSn:example.org"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233))); assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org"); assert_eq!(ev.sender, "@alice:example.org"); assert_eq!(ev.state_key, "@alice:example.org"); assert_eq!(ev.content.avatar_url, None); assert_eq!(ev.content.displayname, None); assert_eq!(ev.content.is_direct, None); assert_eq!(ev.content.membership, MembershipState::Join); assert_matches!(ev.content.third_party_invite, None); let prev_content = ev.unsigned.prev_content.unwrap(); assert_eq!( prev_content.avatar_url.as_deref(), Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF")) ); assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid")); assert_eq!(prev_content.is_direct, Some(true)); assert_eq!(prev_content.membership, MembershipState::Invite); let third_party_invite = prev_content.third_party_invite.unwrap(); assert_eq!(third_party_invite.display_name, "alice"); assert_eq!(third_party_invite.signed.mxid, "@alice:example.org"); assert_eq!( third_party_invite.signed.signatures, btreemap! { server_name!("magic.forest").to_owned() => btreemap! { server_signing_key_id!("ed25519:3").to_owned() => "foobar".to_owned() } } ); assert_eq!(third_party_invite.signed.token, "abc123"); } #[test] fn serde_with_join_authorized() { let json = json!({ "type": "m.room.member", "content": { "membership": "join", "join_authorised_via_users_server": "@notcarl:example.com" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "@carl:example.com" }); let ev = from_json_value::>(json).unwrap(); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.room_id, "!n8f893n9:example.com"); assert_eq!(ev.sender, "@carl:example.com"); assert_eq!(ev.state_key, "@carl:example.com"); assert!(ev.unsigned.is_empty()); assert_eq!(ev.content.avatar_url, None); assert_eq!(ev.content.displayname, None); assert_eq!(ev.content.is_direct, None); assert_eq!(ev.content.membership, MembershipState::Join); assert_matches!(ev.content.third_party_invite, None); assert_eq!( ev.content.join_authorized_via_users_server.as_deref(), Some(user_id!("@notcarl:example.com")) ); } } ruma-common-0.10.5/src/events/room/message/audio.rs000064400000000000000000000171301046102023000203050ustar 00000000000000use std::time::Duration; use js_int::UInt; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3245")] use crate::events::voice::VoiceContent; #[cfg(feature = "unstable-msc3246")] use crate::events::{ audio::AudioContent, file::{EncryptedContent, FileContent, FileContentInfo}, message::MessageContent, }; use crate::{ events::room::{EncryptedFile, MediaSource}, OwnedMxcUri, }; /// The payload for an audio message. /// /// With the `unstable-msc3246` feature, this type contains the transitional format of /// [`AudioEventContent`] and with the `unstable-msc3245` feature, this type also contains the /// transitional format of [`VoiceEventContent`]. See the documentation of the [`message`] module /// for more information. /// /// [`AudioEventContent`]: crate::events::audio::AudioEventContent /// [`VoiceEventContent`]: crate::events::voice::VoiceEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.audio")] #[cfg_attr( feature = "unstable-msc3246", serde(from = "super::content_serde::AudioMessageEventContentDeHelper") )] pub struct AudioMessageEventContent { /// The textual representation of this message. pub body: String, /// The source of the audio clip. #[serde(flatten)] pub source: MediaSource, /// Metadata for the audio clip referred to in `source`. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// Extensible-event text representation of the message. /// /// If present, this should be preferred over the `body` field. #[cfg(feature = "unstable-msc3246")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, /// Extensible-event file content of the message. /// /// If present, this should be preferred over the `source` and `info` fields. #[cfg(feature = "unstable-msc3246")] #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] pub file: Option, /// Extensible-event audio info of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3246")] #[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")] pub audio: Option, /// Extensible-event voice flag of the message. /// /// If present, this should be represented as a voice message. #[cfg(feature = "unstable-msc3245")] #[serde(rename = "org.matrix.msc3245.voice", skip_serializing_if = "Option::is_none")] pub voice: Option, } impl AudioMessageEventContent { /// Creates a new non-encrypted `AudioMessageEventContent` with the given body, url and /// optional extra info. pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> Self { Self { #[cfg(feature = "unstable-msc3246")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3246")] file: Some(FileContent::plain( url.clone(), info.as_deref().and_then(|info| { FileContentInfo::from_room_message_content( None, info.mimetype.to_owned(), info.size.to_owned(), ) .map(Box::new) }), )), #[cfg(feature = "unstable-msc3246")] audio: Some( info.as_deref() .and_then(|info| info.duration) .map(AudioContent::from_room_message_content) .unwrap_or_default(), ), #[cfg(feature = "unstable-msc3245")] voice: None, body, source: MediaSource::Plain(url), info, } } /// Creates a new encrypted `AudioMessageEventContent` with the given body and encrypted /// file. pub fn encrypted(body: String, file: EncryptedFile) -> Self { Self { #[cfg(feature = "unstable-msc3246")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3246")] file: Some(FileContent::encrypted( file.url.clone(), EncryptedContent::from(&file), None, )), #[cfg(feature = "unstable-msc3246")] audio: Some(AudioContent::default()), #[cfg(feature = "unstable-msc3245")] voice: None, body, source: MediaSource::Encrypted(Box::new(file)), info: None, } } /// Create a new `AudioMessageEventContent` with the given message, file info and audio info. #[cfg(feature = "unstable-msc3246")] pub(crate) fn from_extensible_content( message: MessageContent, file: FileContent, audio: AudioContent, ) -> Self { let body = if let Some(body) = message.find_plain() { body.to_owned() } else { message[0].body.clone() }; let source = (&file).into(); let info = AudioInfo::from_extensible_content(file.info.as_deref(), &audio).map(Box::new); Self { message: Some(message), file: Some(file), audio: Some(audio), #[cfg(feature = "unstable-msc3245")] voice: None, body, source, info, } } /// Create a new `AudioMessageEventContent` with the given message, file info, audio info and /// voice flag. #[cfg(feature = "unstable-msc3245")] pub fn from_extensible_voice_content( message: MessageContent, file: FileContent, audio: AudioContent, voice: VoiceContent, ) -> Self { let mut content = Self::from_extensible_content(message, file, audio); content.voice = Some(voice); content } } /// Metadata about an audio clip. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct AudioInfo { /// The duration of the audio in milliseconds. #[serde( with = "crate::serde::duration::opt_ms", default, skip_serializing_if = "Option::is_none" )] pub duration: Option, /// The mimetype of the audio, e.g. "audio/aac". #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The size of the audio clip in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, } impl AudioInfo { /// Creates an empty `AudioInfo`. pub fn new() -> Self { Self::default() } /// Create an `AudioInfo` from the given file info and audio info. /// /// Returns `None` if the `AudioInfo` would be empty. #[cfg(feature = "unstable-msc3246")] fn from_extensible_content( file_info: Option<&FileContentInfo>, audio: &AudioContent, ) -> Option { let (mimetype, size) = file_info .map(|info| { let FileContentInfo { mimetype, size, .. } = info; (mimetype.to_owned(), size.to_owned()) }) .unwrap_or_default(); let AudioContent { duration, .. } = audio; if duration.is_none() && mimetype.is_none() && size.is_none() { None } else { Some(Self { duration: duration.to_owned(), mimetype, size }) } } } ruma-common-0.10.5/src/events/room/message/content_serde.rs000064400000000000000000000374251046102023000220510ustar 00000000000000//! `Deserialize` implementation for RoomMessageEventContent and MessageType. use serde::{de, Deserialize}; use serde_json::value::RawValue as RawJsonValue; #[cfg(feature = "unstable-msc3552")] use super::ImageMessageEventContent; #[cfg(feature = "unstable-msc3246")] use super::{AudioInfo, AudioMessageEventContent}; #[cfg(feature = "unstable-msc3551")] use super::{FileInfo, FileMessageEventContent}; #[cfg(feature = "unstable-msc3488")] use super::{LocationInfo, LocationMessageEventContent}; use super::{MessageType, Relation, RoomMessageEventContent}; #[cfg(feature = "unstable-msc3553")] use super::{VideoInfo, VideoMessageEventContent}; #[cfg(feature = "unstable-msc3246")] use crate::events::audio::AudioContent; #[cfg(feature = "unstable-msc3551")] use crate::events::file::FileContent; #[cfg(feature = "unstable-msc3552")] use crate::events::image::{ImageContent, ThumbnailContent}; #[cfg(feature = "unstable-msc3488")] use crate::events::location::{AssetContent, LocationContent}; #[cfg(any( feature = "unstable-msc3246", feature = "unstable-msc3488", feature = "unstable-msc3551" ))] use crate::events::message::MessageContent; #[cfg(feature = "unstable-msc3552")] use crate::events::room::ImageInfo; #[cfg(feature = "unstable-msc3551")] use crate::events::room::MediaSource; #[cfg(feature = "unstable-msc3553")] use crate::events::video::VideoContent; #[cfg(feature = "unstable-msc3245")] use crate::events::voice::VoiceContent; use crate::serde::from_raw_json_value; #[cfg(feature = "unstable-msc3488")] use crate::MilliSecondsSinceUnixEpoch; impl<'de> Deserialize<'de> for RoomMessageEventContent { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let mut deserializer = serde_json::Deserializer::from_str(json.get()); let relates_to = Option::::deserialize(&mut deserializer).map_err(de::Error::custom)?; Ok(Self { msgtype: from_raw_json_value(&json)?, relates_to }) } } /// Helper struct to determine the msgtype from a `serde_json::value::RawValue` #[derive(Debug, Deserialize)] struct MessageTypeDeHelper { /// The message type field msgtype: String, } impl<'de> Deserialize<'de> for MessageType { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let MessageTypeDeHelper { msgtype } = from_raw_json_value(&json)?; Ok(match msgtype.as_ref() { "m.audio" => Self::Audio(from_raw_json_value(&json)?), "m.emote" => Self::Emote(from_raw_json_value(&json)?), "m.file" => Self::File(from_raw_json_value(&json)?), "m.image" => Self::Image(from_raw_json_value(&json)?), "m.location" => Self::Location(from_raw_json_value(&json)?), "m.notice" => Self::Notice(from_raw_json_value(&json)?), "m.server_notice" => Self::ServerNotice(from_raw_json_value(&json)?), "m.text" => Self::Text(from_raw_json_value(&json)?), "m.video" => Self::Video(from_raw_json_value(&json)?), "m.key.verification.request" => Self::VerificationRequest(from_raw_json_value(&json)?), _ => Self::_Custom(from_raw_json_value(&json)?), }) } } /// Helper struct for deserializing `AudioMessageEventContent` with stable and unstable field names. /// /// It's not possible to use the `alias` attribute of serde because of /// https://github.com/serde-rs/serde/issues/1504. #[derive(Clone, Debug, Deserialize)] #[cfg(feature = "unstable-msc3246")] pub struct AudioMessageEventContentDeHelper { /// The textual representation of this message. pub body: String, /// The source of the audio clip. #[serde(flatten)] pub source: MediaSource, /// Metadata for the audio clip referred to in `source`. pub info: Option>, /// Extensible-event text representation of the message. #[serde(flatten)] pub message: Option, /// Extensible-event file content of the message, with stable name. #[serde(rename = "m.file")] pub file_stable: Option, /// Extensible-event file content of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.file")] pub file_unstable: Option, /// Extensible-event audio info of the message, with stable name. #[serde(rename = "m.audio")] pub audio_stable: Option, /// Extensible-event audio info of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.audio")] pub audio_unstable: Option, /// Extensible-event voice flag of the message, with stable name. #[cfg(feature = "unstable-msc3245")] #[serde(rename = "m.voice")] pub voice_stable: Option, /// Extensible-event voice flag of the message, with unstable name. #[cfg(feature = "unstable-msc3245")] #[serde(rename = "org.matrix.msc3245.voice")] pub voice_unstable: Option, } #[cfg(feature = "unstable-msc3246")] impl From for AudioMessageEventContent { fn from(helper: AudioMessageEventContentDeHelper) -> Self { let AudioMessageEventContentDeHelper { body, source, info, message, file_stable, file_unstable, audio_stable, audio_unstable, #[cfg(feature = "unstable-msc3245")] voice_stable, #[cfg(feature = "unstable-msc3245")] voice_unstable, } = helper; let file = file_stable.or(file_unstable); let audio = audio_stable.or(audio_unstable); #[cfg(feature = "unstable-msc3245")] let voice = voice_stable.or(voice_unstable); Self { body, source, info, message, file, audio, #[cfg(feature = "unstable-msc3245")] voice, } } } /// Helper struct for deserializing `FileMessageEventContent` with stable and unstable field names. /// /// It's not possible to use the `alias` attribute of serde because of /// https://github.com/serde-rs/serde/issues/1504. #[derive(Clone, Debug, Deserialize)] #[cfg(feature = "unstable-msc3551")] pub struct FileMessageEventContentDeHelper { /// A human-readable description of the file. pub body: String, /// The original filename of the uploaded file. pub filename: Option, /// The source of the file. #[serde(flatten)] pub source: MediaSource, /// Metadata about the file referred to in `source`. pub info: Option>, /// Extensible-event text representation of the message. #[serde(flatten)] pub message: Option, /// Extensible-event file content of the message, with stable name. #[serde(rename = "m.file")] pub file_stable: Option, /// Extensible-event file content of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.file")] pub file_unstable: Option, } #[cfg(feature = "unstable-msc3551")] impl From for FileMessageEventContent { fn from(helper: FileMessageEventContentDeHelper) -> Self { let FileMessageEventContentDeHelper { body, filename, source, info, message, file_stable, file_unstable, } = helper; let file = file_stable.or(file_unstable); Self { body, filename, source, info, message, file } } } /// Helper struct for deserializing `ImageMessageEventContent` with stable and unstable field names. /// /// It's not possible to use the `alias` attribute of serde because of /// https://github.com/serde-rs/serde/issues/1504. #[derive(Clone, Debug, Deserialize)] #[cfg(feature = "unstable-msc3552")] pub struct ImageMessageEventContentDeHelper { /// A textual representation of the image. pub body: String, /// The source of the image. #[serde(flatten)] pub source: MediaSource, /// Metadata about the image referred to in `source`. pub info: Option>, /// Extensible-event text representation of the message. #[serde(flatten)] pub message: Option, /// Extensible-event file content of the message, with unstable name. #[serde(rename = "m.file")] pub file_stable: Option, /// Extensible-event file content of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.file")] pub file_unstable: Option, /// Extensible-event image info of the message, with stable name. #[serde(rename = "m.image")] pub image_stable: Option>, /// Extensible-event image info of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.image")] pub image_unstable: Option>, /// Extensible-event thumbnails of the message, with stable name. #[serde(rename = "m.thumbnail")] pub thumbnail_stable: Option>, /// Extensible-event thumbnails of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.thumbnail")] pub thumbnail_unstable: Option>, /// Extensible-event captions of the message, with stable name. #[serde(rename = "m.caption")] pub caption_stable: Option, /// Extensible-event captions of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.caption")] pub caption_unstable: Option, } #[cfg(feature = "unstable-msc3552")] impl From for ImageMessageEventContent { fn from(helper: ImageMessageEventContentDeHelper) -> Self { let ImageMessageEventContentDeHelper { body, source, info, message, file_stable, file_unstable, image_stable, image_unstable, thumbnail_stable, thumbnail_unstable, caption_stable, caption_unstable, } = helper; let file = file_stable.or(file_unstable); let image = image_stable.or(image_unstable); let thumbnail = thumbnail_stable.or(thumbnail_unstable); let caption = caption_stable.or(caption_unstable); Self { body, source, info, message, file, image, thumbnail, caption } } } /// Helper struct for deserializing `LocationMessageEventContent` with stable and unstable field /// names. /// /// It's not possible to use the `alias` attribute of serde because of /// https://github.com/serde-rs/serde/issues/1504. #[derive(Clone, Debug, Deserialize)] #[cfg(feature = "unstable-msc3488")] pub struct LocationMessageEventContentDeHelper { /// A description of the location. pub body: String, /// A geo URI representing the location. pub geo_uri: String, /// Info about the location being represented. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// Extensible-event text representation of the message. #[serde(flatten)] pub message: Option, /// Extensible-event location info of the message, with stable name. #[serde(rename = "m.location")] pub location_stable: Option, /// Extensible-event location info of the message, with unstable name. #[serde(rename = "org.matrix.msc3488.location")] pub location_unstable: Option, /// Extensible-event asset this message refers to, with stable name. #[serde(rename = "m.asset")] pub asset_stable: Option, /// Extensible-event asset this message refers to, with unstable name. #[serde(rename = "org.matrix.msc3488.asset")] pub asset_unstable: Option, /// Extensible-event timestamp this message refers to, with stable name. #[serde(rename = "m.ts")] pub ts_stable: Option, /// Extensible-event timestamp this message refers to, with unstable name. #[serde(rename = "org.matrix.msc3488.ts")] pub ts_unstable: Option, } #[cfg(feature = "unstable-msc3488")] impl From for LocationMessageEventContent { fn from(helper: LocationMessageEventContentDeHelper) -> Self { let LocationMessageEventContentDeHelper { body, geo_uri, info, message, location_stable, location_unstable, asset_stable, asset_unstable, ts_stable, ts_unstable, } = helper; let location = location_stable.or(location_unstable); let asset = asset_stable.or(asset_unstable); let ts = ts_stable.or(ts_unstable); Self { body, geo_uri, info, message, location, asset, ts } } } /// Helper struct for deserializing `VideoMessageEventContent` with stable and unstable field names. /// /// It's not possible to use the `alias` attribute of serde because of /// https://github.com/serde-rs/serde/issues/1504. #[derive(Clone, Debug, Deserialize)] #[cfg(feature = "unstable-msc3553")] pub struct VideoMessageEventContentDeHelper { /// A description of the video. pub body: String, /// The source of the video clip. #[serde(flatten)] pub source: MediaSource, /// Metadata about the video clip referred to in `source`. pub info: Option>, /// Extensible-event text representation of the message. #[serde(flatten)] pub message: Option, /// Extensible-event file content of the message, with stable name. #[serde(rename = "m.file")] pub file_stable: Option, /// Extensible-event file content of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.file")] pub file_unstable: Option, /// Extensible-event video info of the message, with stable name. #[serde(rename = "m.video")] pub video_stable: Option>, /// Extensible-event video info of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.video")] pub video_unstable: Option>, /// Extensible-event thumbnails of the message, with stable name. #[serde(rename = "m.thumbnail")] pub thumbnail_stable: Option>, /// Extensible-event thumbnails of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.thumbnail")] pub thumbnail_unstable: Option>, /// Extensible-event captions of the message, with stable name. #[serde(rename = "m.caption")] pub caption_stable: Option, /// Extensible-event captions of the message, with unstable name. #[serde(rename = "org.matrix.msc1767.caption")] pub caption_unstable: Option, } #[cfg(feature = "unstable-msc3553")] impl From for VideoMessageEventContent { fn from(helper: VideoMessageEventContentDeHelper) -> Self { let VideoMessageEventContentDeHelper { body, source, info, message, file_stable, file_unstable, video_stable, video_unstable, thumbnail_stable, thumbnail_unstable, caption_stable, caption_unstable, } = helper; let file = file_stable.or(file_unstable); let video = video_stable.or(video_unstable); let thumbnail = thumbnail_stable.or(thumbnail_unstable); let caption = caption_stable.or(caption_unstable); Self { body, source, info, message, file, video, thumbnail, caption } } } ruma-common-0.10.5/src/events/room/message/emote.rs000064400000000000000000000054261046102023000203220ustar 00000000000000use serde::{Deserialize, Serialize}; use super::FormattedBody; #[cfg(feature = "unstable-msc1767")] use crate::events::message::MessageContent; /// The payload for an emote message. /// /// With the `unstable-msc1767` feature, this type contains the transitional format of /// [`EmoteEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`EmoteEventContent`]: crate::events::emote::EmoteEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.emote")] pub struct EmoteMessageEventContent { /// The emote action to perform. pub body: String, /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, /// Extensible-event representation of the message. /// /// If present, this should be preferred over the other fields. #[cfg(feature = "unstable-msc1767")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, } impl EmoteMessageEventContent { /// A convenience constructor to create a plain-text emote. pub fn plain(body: impl Into) -> Self { let body = body.into(); Self { #[cfg(feature = "unstable-msc1767")] message: Some(MessageContent::plain(body.clone())), body, formatted: None, } } /// A convenience constructor to create an html emote message. pub fn html(body: impl Into, html_body: impl Into) -> Self { let body = body.into(); let html_body = html_body.into(); Self { #[cfg(feature = "unstable-msc1767")] message: Some(MessageContent::html(body.clone(), html_body.clone())), body, formatted: Some(FormattedBody::html(html_body)), } } /// A convenience constructor to create a markdown emote. /// /// Returns an html emote message if some markdown formatting was detected, otherwise returns a /// plain-text emote. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { if let Some(formatted) = FormattedBody::markdown(&body) { Self::html(body, formatted.body) } else { Self::plain(body) } } } #[cfg(feature = "unstable-msc1767")] impl From for EmoteMessageEventContent { fn from(message: MessageContent) -> Self { let body = if let Some(body) = message.find_plain() { body } else { &message[0].body }; let formatted = message.find_html().map(FormattedBody::html); Self { body: body.to_owned(), formatted, message: Some(message) } } } ruma-common-0.10.5/src/events/room/message/file.rs000064400000000000000000000132511046102023000201230ustar 00000000000000use js_int::UInt; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3551")] use crate::events::{ file::{FileContent, FileContentInfo}, message::MessageContent, }; use crate::{ events::room::{EncryptedFile, MediaSource, ThumbnailInfo}, OwnedMxcUri, }; /// The payload for a file message. /// /// With the `unstable-msc3551` feature, this type contains the transitional format of /// [`FileEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`FileEventContent`]: crate::events::file::FileEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.file")] #[cfg_attr( feature = "unstable-msc3551", serde(from = "super::content_serde::FileMessageEventContentDeHelper") )] pub struct FileMessageEventContent { /// A human-readable description of the file. /// /// This is recommended to be the filename of the original upload. pub body: String, /// The original filename of the uploaded file. #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option, /// The source of the file. #[serde(flatten)] pub source: MediaSource, /// Metadata about the file referred to in `source`. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// Extensible-event text representation of the message. /// /// If present, this should be preferred over the `body` field. #[cfg(feature = "unstable-msc3551")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, /// Extensible-event file content of the message. /// /// If present, this should be preferred over the `source` and `info` fields. #[cfg(feature = "unstable-msc3551")] #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] pub file: Option, } impl FileMessageEventContent { /// Creates a new non-encrypted `FileMessageEventContent` with the given body, url and /// optional extra info. pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> Self { Self { #[cfg(feature = "unstable-msc3551")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3551")] file: Some(FileContent::plain( url.clone(), info.as_deref().and_then(|info| { FileContentInfo::from_room_message_content( None, info.mimetype.to_owned(), info.size.to_owned(), ) .map(Box::new) }), )), body, filename: None, source: MediaSource::Plain(url), info, } } /// Creates a new encrypted `FileMessageEventContent` with the given body and encrypted /// file. pub fn encrypted(body: String, file: EncryptedFile) -> Self { Self { #[cfg(feature = "unstable-msc3551")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3551")] file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)), body, filename: None, source: MediaSource::Encrypted(Box::new(file)), info: None, } } /// Create a new `FileMessageEventContent` with the given message and file info. #[cfg(feature = "unstable-msc3551")] pub(crate) fn from_extensible_content(message: MessageContent, file: FileContent) -> Self { let body = if let Some(body) = message.find_plain() { body.to_owned() } else { message[0].body.clone() }; let filename = file.info.as_deref().and_then(|info| info.name.clone()); let info = file.info.as_deref().and_then(|info| { FileInfo::from_extensible_content(info.mimetype.to_owned(), info.size.to_owned()) .map(Box::new) }); let source = (&file).into(); Self { message: Some(message), file: Some(file), body, filename, source, info } } } /// Metadata about a file. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct FileInfo { /// The mimetype of the file, e.g. "application/msword". #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The size of the file in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, /// Metadata about the image referred to in `thumbnail_source`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option>, /// The source of the thumbnail of the file. #[serde( flatten, with = "crate::events::room::thumbnail_source_serde", skip_serializing_if = "Option::is_none" )] pub thumbnail_source: Option, } impl FileInfo { /// Creates an empty `FileInfo`. pub fn new() -> Self { Self::default() } /// Creates a `FileInfo` with the given optional mimetype and size. /// /// Returns `None` if the `FileInfo` would be empty. #[cfg(feature = "unstable-msc3551")] fn from_extensible_content(mimetype: Option, size: Option) -> Option { if mimetype.is_none() && size.is_none() { None } else { Some(Self { mimetype, size, ..Default::default() }) } } } ruma-common-0.10.5/src/events/room/message/image.rs000064400000000000000000000146451046102023000202760ustar 00000000000000use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3552")] use crate::events::{ file::{FileContent, FileContentInfo}, image::{ImageContent, ThumbnailContent}, message::MessageContent, }; use crate::{ events::room::{EncryptedFile, ImageInfo, MediaSource}, OwnedMxcUri, }; /// The payload for an image message. /// /// With the `unstable-msc3552` feature, this type contains the transitional format of /// [`ImageEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`ImageEventContent`]: crate::events::image::ImageEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.image")] #[cfg_attr( feature = "unstable-msc3552", serde(from = "super::content_serde::ImageMessageEventContentDeHelper") )] pub struct ImageMessageEventContent { /// A textual representation of the image. /// /// Could be the alt text of the image, the filename of the image, or some kind of content /// description for accessibility e.g. "image attachment". pub body: String, /// The source of the image. #[serde(flatten)] pub source: MediaSource, /// Metadata about the image referred to in `source`. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// Extensible-event text representation of the message. /// /// If present, this should be preferred over the `body` field. #[cfg(feature = "unstable-msc3552")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, /// Extensible-event file content of the message. /// /// If present, this should be preferred over the `source` and `info` fields. #[cfg(feature = "unstable-msc3552")] #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] pub file: Option, /// Extensible-event image info of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3552")] #[serde(rename = "org.matrix.msc1767.image", skip_serializing_if = "Option::is_none")] pub image: Option>, /// Extensible-event thumbnails of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3552")] #[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")] pub thumbnail: Option>, /// Extensible-event captions of the message. #[cfg(feature = "unstable-msc3552")] #[serde( rename = "org.matrix.msc1767.caption", with = "crate::events::message::content_serde::as_vec", default, skip_serializing_if = "Option::is_none" )] pub caption: Option, } impl ImageMessageEventContent { /// Creates a new non-encrypted `ImageMessageEventContent` with the given body, url and /// optional extra info. pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> Self { Self { #[cfg(feature = "unstable-msc3552")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3552")] file: Some(FileContent::plain( url.clone(), info.as_deref().and_then(|info| { FileContentInfo::from_room_message_content( None, info.mimetype.to_owned(), info.size, ) .map(Box::new) }), )), #[cfg(feature = "unstable-msc3552")] image: Some(Box::new( info.as_deref() .and_then(|info| { ImageContent::from_room_message_content(info.width, info.height) }) .unwrap_or_default(), )), #[cfg(feature = "unstable-msc3552")] thumbnail: info .as_deref() .and_then(|info| { ThumbnailContent::from_room_message_content( info.thumbnail_source.to_owned(), info.thumbnail_info.to_owned(), ) }) .map(|thumbnail| vec![thumbnail]), #[cfg(feature = "unstable-msc3552")] caption: None, body, source: MediaSource::Plain(url), info, } } /// Creates a new encrypted `ImageMessageEventContent` with the given body and encrypted /// file. pub fn encrypted(body: String, file: EncryptedFile) -> Self { Self { #[cfg(feature = "unstable-msc3552")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3552")] file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)), #[cfg(feature = "unstable-msc3552")] image: Some(Box::new(ImageContent::default())), #[cfg(feature = "unstable-msc3552")] thumbnail: None, #[cfg(feature = "unstable-msc3552")] caption: None, body, source: MediaSource::Encrypted(Box::new(file)), info: None, } } /// Create a new `ImageMessageEventContent` with the given message, file info, image info, /// thumbnails and captions. #[cfg(feature = "unstable-msc3552")] pub(crate) fn from_extensible_content( message: MessageContent, file: FileContent, image: Box, thumbnail: Vec, caption: Option, ) -> Self { let body = if let Some(body) = message.find_plain() { body.to_owned() } else { message[0].body.clone() }; let source = (&file).into(); let info = ImageInfo::from_extensible_content(file.info.as_deref(), &image, &thumbnail) .map(Box::new); let thumbnail = if thumbnail.is_empty() { None } else { Some(thumbnail) }; Self { message: Some(message), file: Some(file), image: Some(image), thumbnail, caption, body, source, info, } } } ruma-common-0.10.5/src/events/room/message/key_verification_request.rs000064400000000000000000000027031046102023000243060ustar 00000000000000use serde::{Deserialize, Serialize}; use crate::{events::key::verification::VerificationMethod, OwnedDeviceId, OwnedUserId}; /// The payload for a key verification request message. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.key.verification.request")] pub struct KeyVerificationRequestEventContent { /// A fallback message to alert users that their client does not support the key verification /// framework. pub body: String, /// The verification methods supported by the sender. pub methods: Vec, /// The device ID which is initiating the request. pub from_device: OwnedDeviceId, /// The user ID which should receive the request. /// /// Users should only respond to verification requests if they are named in this field. Users /// who are not named in this field and who did not send this event should ignore all other /// events that have a `m.reference` relationship with this event. pub to: OwnedUserId, } impl KeyVerificationRequestEventContent { /// Creates a new `KeyVerificationRequestEventContent` with the given body, method, device /// and user ID. pub fn new( body: String, methods: Vec, from_device: OwnedDeviceId, to: OwnedUserId, ) -> Self { Self { body, methods, from_device, to } } } ruma-common-0.10.5/src/events/room/message/location.rs000064400000000000000000000107711046102023000210200ustar 00000000000000use serde::{Deserialize, Serialize}; use crate::events::room::{MediaSource, ThumbnailInfo}; #[cfg(feature = "unstable-msc3488")] use crate::{ events::{ location::{AssetContent, LocationContent}, message::MessageContent, }, MilliSecondsSinceUnixEpoch, }; /// The payload for a location message. /// /// With the `unstable-msc3488` feature, this type contains the transitional format of /// [`LocationEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`LocationEventContent`]: crate::events::location::LocationEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.location")] #[cfg_attr( feature = "unstable-msc3488", serde(from = "super::content_serde::LocationMessageEventContentDeHelper") )] pub struct LocationMessageEventContent { /// A description of the location e.g. "Big Ben, London, UK", or some kind of content /// description for accessibility, e.g. "location attachment". pub body: String, /// A geo URI representing the location. pub geo_uri: String, /// Info about the location being represented. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// Extensible-event text representation of the message. /// /// If present, this should be preferred over the `body` field. #[cfg(feature = "unstable-msc3488")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, /// Extensible-event location info of the message. /// /// If present, this should be preferred over the `geo_uri` field. #[cfg(feature = "unstable-msc3488")] #[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")] pub location: Option, /// Extensible-event asset this message refers to. #[cfg(feature = "unstable-msc3488")] #[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")] pub asset: Option, /// Extensible-event timestamp this message refers to. #[cfg(feature = "unstable-msc3488")] #[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")] pub ts: Option, } impl LocationMessageEventContent { /// Creates a new `LocationMessageEventContent` with the given body and geo URI. pub fn new(body: String, geo_uri: String) -> Self { Self { #[cfg(feature = "unstable-msc3488")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3488")] location: Some(LocationContent::new(geo_uri.clone())), #[cfg(feature = "unstable-msc3488")] asset: None, #[cfg(feature = "unstable-msc3488")] ts: None, body, geo_uri, info: None, } } /// Create a new `LocationMessageEventContent` with the given message, location info, asset and /// timestamp. #[cfg(feature = "unstable-msc3488")] pub(crate) fn from_extensible_content( message: MessageContent, location: LocationContent, asset: AssetContent, ts: Option, ) -> Self { let body = if let Some(body) = message.find_plain() { body.to_owned() } else { message[0].body.clone() }; let geo_uri = location.uri.clone(); Self { message: Some(message), location: Some(location), asset: Some(asset), ts, body, geo_uri, info: None, } } } /// Thumbnail info associated with a location. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct LocationInfo { /// The source of a thumbnail of the location. #[serde( flatten, with = "crate::events::room::thumbnail_source_serde", skip_serializing_if = "Option::is_none" )] pub thumbnail_source: Option, /// Metadata about the image referred to in `thumbnail_source. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option>, } impl LocationInfo { /// Creates an empty `LocationInfo`. pub fn new() -> Self { Self::default() } } ruma-common-0.10.5/src/events/room/message/notice.rs000064400000000000000000000054101046102023000204630ustar 00000000000000use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc1767")] use crate::events::message::MessageContent; use super::FormattedBody; /// The payload for a notice message. /// /// With the `unstable-msc1767` feature, this type contains the transitional format of /// [`NoticeEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`NoticeEventContent`]: crate::events::notice::NoticeEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.notice")] pub struct NoticeMessageEventContent { /// The notice text. pub body: String, /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, /// Extensible-event representation of the message. /// /// If present, this should be preferred over the other fields. #[cfg(feature = "unstable-msc1767")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, } impl NoticeMessageEventContent { /// A convenience constructor to create a plain text notice. pub fn plain(body: impl Into) -> Self { let body = body.into(); Self { #[cfg(feature = "unstable-msc1767")] message: Some(MessageContent::plain(body.clone())), body, formatted: None, } } /// A convenience constructor to create an html notice. pub fn html(body: impl Into, html_body: impl Into) -> Self { let body = body.into(); let html_body = html_body.into(); Self { #[cfg(feature = "unstable-msc1767")] message: Some(MessageContent::html(body.clone(), html_body.clone())), body, formatted: Some(FormattedBody::html(html_body)), } } /// A convenience constructor to create a markdown notice. /// /// Returns an html notice if some markdown formatting was detected, otherwise returns a plain /// text notice. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { if let Some(formatted) = FormattedBody::markdown(&body) { Self::html(body, formatted.body) } else { Self::plain(body) } } } #[cfg(feature = "unstable-msc1767")] impl From for NoticeMessageEventContent { fn from(message: MessageContent) -> Self { let body = if let Some(body) = message.find_plain() { body } else { &message[0].body }; let formatted = message.find_html().map(FormattedBody::html); Self { body: body.to_owned(), formatted, message: Some(message) } } } ruma-common-0.10.5/src/events/room/message/relation_serde.rs000064400000000000000000000160731046102023000222100ustar 00000000000000use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "unstable-msc2676")] use super::Replacement; #[cfg(feature = "unstable-msc2676")] use super::RoomMessageEventContent; #[cfg(feature = "unstable-msc3440")] use super::Thread; use super::{InReplyTo, Relation}; #[cfg(any(feature = "unstable-msc2676", feature = "unstable-msc3440"))] use crate::OwnedEventId; impl<'de> Deserialize<'de> for Relation { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let ev = EventWithRelatesToJsonRepr::deserialize(deserializer)?; #[cfg(feature = "unstable-msc3440")] if let Some( RelationJsonRepr::ThreadStable(ThreadStableJsonRepr { event_id, is_falling_back }) | RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr { event_id, is_falling_back }), ) = ev.relates_to.relation { let in_reply_to = ev .relates_to .in_reply_to .ok_or_else(|| serde::de::Error::missing_field("m.in_reply_to"))?; return Ok(Relation::Thread(Thread { event_id, in_reply_to, is_falling_back })); } let rel = if let Some(in_reply_to) = ev.relates_to.in_reply_to { Relation::Reply { in_reply_to } } else if let Some(relation) = ev.relates_to.relation { match relation { #[cfg(feature = "unstable-msc2676")] RelationJsonRepr::Replacement(ReplacementJsonRepr { event_id }) => { let new_content = ev .new_content .ok_or_else(|| serde::de::Error::missing_field("m.new_content"))?; Relation::Replacement(Replacement { event_id, new_content }) } // FIXME: Maybe we should log this, though at this point we don't even have // access to the rel_type of the unknown relation. RelationJsonRepr::Unknown => Relation::_Custom, #[cfg(feature = "unstable-msc3440")] RelationJsonRepr::ThreadStable(_) | RelationJsonRepr::ThreadUnstable(_) => { unreachable!() } } } else { Relation::_Custom }; Ok(rel) } } impl Serialize for Relation { fn serialize(&self, serializer: S) -> Result where S: Serializer, { #[allow(clippy::needless_update)] let json_repr = match self { Relation::Reply { in_reply_to } => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr { in_reply_to: Some(in_reply_to.clone()), ..Default::default() }), #[cfg(feature = "unstable-msc2676")] Relation::Replacement(Replacement { event_id, new_content }) => { EventWithRelatesToJsonRepr { relates_to: RelatesToJsonRepr { relation: Some(RelationJsonRepr::Replacement(ReplacementJsonRepr { event_id: event_id.clone(), })), ..Default::default() }, new_content: Some(new_content.clone()), } } #[cfg(feature = "unstable-msc3440")] Relation::Thread(Thread { event_id, in_reply_to, is_falling_back }) => { EventWithRelatesToJsonRepr::new(RelatesToJsonRepr { in_reply_to: Some(in_reply_to.clone()), relation: Some(RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr { event_id: event_id.clone(), is_falling_back: *is_falling_back, })), ..Default::default() }) } Relation::_Custom => EventWithRelatesToJsonRepr::default(), }; json_repr.serialize(serializer) } } #[derive(Default, Deserialize, Serialize)] struct EventWithRelatesToJsonRepr { #[serde(rename = "m.relates_to", default, skip_serializing_if = "RelatesToJsonRepr::is_empty")] relates_to: RelatesToJsonRepr, #[cfg(feature = "unstable-msc2676")] #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] new_content: Option>, } impl EventWithRelatesToJsonRepr { fn new(relates_to: RelatesToJsonRepr) -> Self { Self { relates_to, #[cfg(feature = "unstable-msc2676")] new_content: None, } } } /// Struct modeling the different ways relationships can be expressed in a `m.relates_to` field of /// an event. #[derive(Default, Deserialize, Serialize)] struct RelatesToJsonRepr { #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")] in_reply_to: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] relation: Option, } impl RelatesToJsonRepr { fn is_empty(&self) -> bool { self.in_reply_to.is_none() && self.relation.is_none() } } /// A relation, which associates new information to an existing event. #[derive(Clone, Deserialize, Serialize)] #[serde(tag = "rel_type")] enum RelationJsonRepr { /// An event that replaces another event. #[cfg(feature = "unstable-msc2676")] #[serde(rename = "m.replace")] Replacement(ReplacementJsonRepr), /// An event that belongs to a thread, with unstable names. #[cfg(feature = "unstable-msc3440")] #[serde(rename = "m.thread")] ThreadStable(ThreadStableJsonRepr), /// An event that belongs to a thread, with unstable names. #[cfg(feature = "unstable-msc3440")] #[serde(rename = "io.element.thread")] ThreadUnstable(ThreadUnstableJsonRepr), /// An unknown relation type. /// /// Not available in the public API, but exists here so deserialization /// doesn't fail with new / custom `rel_type`s. #[serde(other)] Unknown, } #[derive(Clone, Deserialize, Serialize)] #[cfg(feature = "unstable-msc2676")] struct ReplacementJsonRepr { event_id: OwnedEventId, } /// A thread relation without the reply fallback, with stable names. #[derive(Clone, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] struct ThreadStableJsonRepr { /// The ID of the root message in the thread. event_id: OwnedEventId, /// Whether the `m.in_reply_to` field is a fallback for older clients or a real reply in a /// thread. #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] is_falling_back: bool, } /// A thread relation without the reply fallback, with unstable names. #[derive(Clone, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] struct ThreadUnstableJsonRepr { /// The ID of the root message in the thread. event_id: OwnedEventId, /// Whether the `m.in_reply_to` field is a fallback for older clients or a real reply in a /// thread. #[serde( rename = "io.element.show_reply", default, skip_serializing_if = "ruma_common::serde::is_default" )] is_falling_back: bool, } ruma-common-0.10.5/src/events/room/message/reply.rs000064400000000000000000000137331046102023000203440ustar 00000000000000use std::fmt; use super::{ sanitize::remove_plain_reply_fallback, FormattedBody, MessageType, OriginalRoomMessageEvent, Relation, }; #[cfg(feature = "unstable-sanitize")] use super::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback}; fn get_message_quote_fallbacks(original_message: &OriginalRoomMessageEvent) -> (String, String) { let get_quotes = |body: &str, formatted: Option<&FormattedBody>, is_emote: bool| { let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = original_message; let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. })); let emote_sign = is_emote.then(|| "* ").unwrap_or_default(); let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body); #[cfg(feature = "unstable-sanitize")] let html_body = formatted_or_plain_body(formatted, body, is_reply); #[cfg(not(feature = "unstable-sanitize"))] let html_body = formatted_or_plain_body(formatted, body); ( format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "), format!( "\ \ " ), ) }; match &original_message.content.msgtype { MessageType::Audio(_) => get_quotes("sent an audio file.", None, false), MessageType::Emote(c) => get_quotes(&c.body, c.formatted.as_ref(), true), MessageType::File(_) => get_quotes("sent a file.", None, false), MessageType::Image(_) => get_quotes("sent an image.", None, false), MessageType::Location(_) => get_quotes("sent a location.", None, false), MessageType::Notice(c) => get_quotes(&c.body, c.formatted.as_ref(), false), MessageType::ServerNotice(c) => get_quotes(&c.body, None, false), MessageType::Text(c) => get_quotes(&c.body, c.formatted.as_ref(), false), MessageType::Video(_) => get_quotes("sent a video.", None, false), MessageType::VerificationRequest(content) => get_quotes(&content.body, None, false), MessageType::_Custom(content) => get_quotes(&content.body, None, false), } } fn formatted_or_plain_body( formatted: Option<&FormattedBody>, body: &str, #[cfg(feature = "unstable-sanitize")] is_reply: bool, ) -> String { if let Some(formatted_body) = formatted { #[cfg(feature = "unstable-sanitize")] if is_reply { sanitize_html(&formatted_body.body, HtmlSanitizerMode::Strict, RemoveReplyFallback::Yes) } else { formatted_body.body.clone() } #[cfg(not(feature = "unstable-sanitize"))] formatted_body.body.clone() } else { let mut escaped_body = String::with_capacity(body.len()); for c in body.chars() { // Escape reserved HTML entities and new lines. // let s = match c { '&' => Some("&"), '<' => Some("<"), '>' => Some(">"), '"' => Some("""), '\n' => Some("
"), _ => None, }; if let Some(s) = s { escaped_body.push_str(s); } else { escaped_body.push(c); } } escaped_body } } /// Get the plain and formatted body for a rich reply. /// /// Returns a `(plain, html)` tuple. /// /// With the `sanitize` feature, [HTML tags and attributes] that are not allowed in the Matrix /// spec and previous [rich reply fallbacks] are removed from the previous message in the new rich /// reply fallback. /// /// [HTML tags and attributes]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes /// [rich reply fallbacks]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies pub fn plain_and_formatted_reply_body( body: impl fmt::Display, formatted: Option, original_message: &OriginalRoomMessageEvent, ) -> (String, String) { let (quoted, quoted_html) = get_message_quote_fallbacks(original_message); let plain = format!("{quoted}\n{body}"); let html = match formatted { Some(formatted) => format!("{quoted_html}{formatted}"), None => format!("{quoted_html}{body}"), }; (plain, html) } #[cfg(test)] mod tests { use crate::{ event_id, events::{room::message::RoomMessageEventContent, MessageLikeUnsigned}, room_id, user_id, MilliSecondsSinceUnixEpoch, }; use super::OriginalRoomMessageEvent; #[test] fn fallback_multiline() { let (plain_quote, html_quote) = super::get_message_quote_fallbacks(&OriginalRoomMessageEvent { content: RoomMessageEventContent::text_plain("multi\nline"), event_id: event_id!("$1598361704261elfgc:localhost").to_owned(), sender: user_id!("@alice:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch::now(), room_id: room_id!("!n8f893n9:example.com").to_owned(), unsigned: MessageLikeUnsigned::new(), }); assert_eq!(plain_quote, "> <@alice:example.com> multi\n> line"); assert_eq!( html_quote, "\
\ In reply to \ @alice:example.com\
\ multi
line\
\
", ); } } ruma-common-0.10.5/src/events/room/message/sanitize/html_fragment.rs000064400000000000000000000275361046102023000236740ustar 00000000000000use std::{collections::BTreeSet, fmt, io}; use html5ever::{ local_name, namespace_url, ns, parse_fragment, serialize::{serialize, Serialize, SerializeOpts, Serializer, TraversalScope}, tendril::{StrTendril, TendrilSink}, tree_builder::{NodeOrText, TreeSink}, Attribute, ParseOpts, QualName, }; use tracing::debug; /// An HTML fragment. /// /// To get the serialized HTML, use its `Display` implementation. #[derive(Debug)] pub struct Fragment { pub nodes: Vec, } impl Fragment { /// Construct a new `Fragment` by parsing the given HTML. pub fn parse_html(html: &str) -> Self { let sink = Self::default(); let mut parser = parse_fragment( sink, ParseOpts::default(), QualName::new(None, ns!(html), local_name!("div")), Vec::new(), ); parser.process(html.into()); parser.finish() } /// Construct a new `Node` with the given data and add it to this `Fragment`. /// /// Returns the index of the new node. pub fn new_node(&mut self, data: NodeData) -> usize { self.nodes.push(Node::new(data)); self.nodes.len() - 1 } /// Append the given node to the given parent in this `Fragment`. /// /// The node is detached from its previous position. pub fn append_node(&mut self, parent_id: usize, node_id: usize) { self.detach(node_id); self.nodes[node_id].parent = Some(parent_id); if let Some(last_child) = self.nodes[parent_id].last_child.take() { self.nodes[node_id].prev_sibling = Some(last_child); self.nodes[last_child].next_sibling = Some(node_id); } else { self.nodes[parent_id].first_child = Some(node_id); } self.nodes[parent_id].last_child = Some(node_id); } /// Insert the given node before the given sibling in this `Fragment`. /// /// The node is detached from its previous position. pub fn insert_before(&mut self, sibling_id: usize, node_id: usize) { self.detach(node_id); self.nodes[node_id].parent = self.nodes[sibling_id].parent; self.nodes[node_id].next_sibling = Some(sibling_id); if let Some(prev_sibling) = self.nodes[sibling_id].prev_sibling.take() { self.nodes[node_id].prev_sibling = Some(prev_sibling); self.nodes[prev_sibling].next_sibling = Some(node_id); } else if let Some(parent) = self.nodes[sibling_id].parent { self.nodes[parent].first_child = Some(node_id); } self.nodes[sibling_id].prev_sibling = Some(node_id); } /// Detach the given node from this `Fragment`. pub fn detach(&mut self, node_id: usize) { let (parent, prev_sibling, next_sibling) = { let node = &mut self.nodes[node_id]; (node.parent.take(), node.prev_sibling.take(), node.next_sibling.take()) }; if let Some(next_sibling) = next_sibling { self.nodes[next_sibling].prev_sibling = prev_sibling; } else if let Some(parent) = parent { self.nodes[parent].last_child = prev_sibling; } if let Some(prev_sibling) = prev_sibling { self.nodes[prev_sibling].next_sibling = next_sibling; } else if let Some(parent) = parent { self.nodes[parent].first_child = next_sibling; } } } impl Default for Fragment { fn default() -> Self { Self { nodes: vec![Node::new(NodeData::Document)] } } } impl TreeSink for Fragment { type Handle = usize; type Output = Self; fn finish(self) -> Self::Output { self } fn parse_error(&mut self, msg: std::borrow::Cow<'static, str>) { debug!("HTML parse error: {msg}"); } fn get_document(&mut self) -> Self::Handle { 0 } fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> html5ever::ExpandedName<'a> { self.nodes[*target].as_element().expect("not an element").name.expanded() } fn create_element( &mut self, name: QualName, attrs: Vec, _flags: html5ever::tree_builder::ElementFlags, ) -> Self::Handle { self.new_node(NodeData::Element(ElementData { name, attrs: attrs.into_iter().collect() })) } fn create_comment(&mut self, _text: StrTendril) -> Self::Handle { self.new_node(NodeData::Other) } fn create_pi(&mut self, _target: StrTendril, _data: StrTendril) -> Self::Handle { self.new_node(NodeData::Other) } fn append(&mut self, parent: &Self::Handle, child: NodeOrText) { match child { NodeOrText::AppendNode(index) => self.append_node(*parent, index), NodeOrText::AppendText(text) => { // If the previous sibling is also text, add this text to it. if let Some(sibling) = self.nodes[*parent].last_child.and_then(|child| self.nodes[child].as_text_mut()) { sibling.push_tendril(&text); } else { let index = self.new_node(NodeData::Text(text)); self.append_node(*parent, index); } } } } fn append_based_on_parent_node( &mut self, element: &Self::Handle, prev_element: &Self::Handle, child: NodeOrText, ) { if self.nodes[*element].parent.is_some() { self.append_before_sibling(element, child); } else { self.append(prev_element, child); } } fn append_doctype_to_document( &mut self, _name: StrTendril, _public_id: StrTendril, _system_id: StrTendril, ) { } fn get_template_contents(&mut self, target: &Self::Handle) -> Self::Handle { *target } fn same_node(&self, x: &Self::Handle, y: &Self::Handle) -> bool { x == y } fn set_quirks_mode(&mut self, _mode: html5ever::tree_builder::QuirksMode) {} fn append_before_sibling( &mut self, sibling: &Self::Handle, new_node: NodeOrText, ) { match new_node { NodeOrText::AppendNode(index) => self.insert_before(*sibling, index), NodeOrText::AppendText(text) => { // If the previous sibling is also text, add this text to it. if let Some(prev_text) = self.nodes[*sibling] .prev_sibling .and_then(|prev| self.nodes[prev].as_text_mut()) { prev_text.push_tendril(&text); } else { let index = self.new_node(NodeData::Text(text)); self.insert_before(*sibling, index); } } } } fn add_attrs_if_missing(&mut self, target: &Self::Handle, attrs: Vec) { let target = self.nodes[*target].as_element_mut().unwrap(); target.attrs.extend(attrs.into_iter()); } fn remove_from_parent(&mut self, target: &Self::Handle) { self.detach(*target); } fn reparent_children(&mut self, node: &Self::Handle, new_parent: &Self::Handle) { let mut next_child = self.nodes[*node].first_child; while let Some(child) = next_child { next_child = self.nodes[child].next_sibling; self.append_node(*new_parent, child); } } } impl Serialize for Fragment { fn serialize(&self, serializer: &mut S, traversal_scope: TraversalScope) -> io::Result<()> where S: Serializer, { match traversal_scope { TraversalScope::IncludeNode => { let root = self.nodes[0].first_child.unwrap(); let mut next_child = self.nodes[root].first_child; while let Some(child) = next_child { let child = &self.nodes[child]; child.serialize(self, serializer)?; next_child = child.next_sibling; } Ok(()) } TraversalScope::ChildrenOnly(_) => Ok(()), } } } impl fmt::Display for Fragment { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut u8_vec = Vec::new(); serialize( &mut u8_vec, self, SerializeOpts { traversal_scope: TraversalScope::IncludeNode, ..Default::default() }, ) .unwrap(); f.write_str(&String::from_utf8(u8_vec).unwrap())?; Ok(()) } } /// An HTML node. #[derive(Debug)] pub struct Node { pub parent: Option, pub prev_sibling: Option, pub next_sibling: Option, pub first_child: Option, pub last_child: Option, pub data: NodeData, } impl Node { /// Constructs a new `Node` with the given data. pub fn new(data: NodeData) -> Self { Self { parent: None, prev_sibling: None, next_sibling: None, first_child: None, last_child: None, data, } } /// Returns the `ElementData` of this `Node` if it is a `NodeData::Element`. pub fn as_element(&self) -> Option<&ElementData> { match &self.data { NodeData::Element(data) => Some(data), _ => None, } } /// Returns the mutable `ElementData` of this `Node` if it is a `NodeData::Element`. pub fn as_element_mut(&mut self) -> Option<&mut ElementData> { match &mut self.data { NodeData::Element(data) => Some(data), _ => None, } } /// Returns the mutable text content of this `Node`, if it is a `NodeData::Text`. pub fn as_text_mut(&mut self) -> Option<&mut StrTendril> { match &mut self.data { NodeData::Text(data) => Some(data), _ => None, } } } impl Node { pub fn serialize(&self, fragment: &Fragment, serializer: &mut S) -> io::Result<()> where S: Serializer, { match &self.data { NodeData::Element(data) => { serializer.start_elem( data.name.clone(), data.attrs.iter().map(|attr| (&attr.name, &*attr.value)), )?; let mut next_child = self.first_child; while let Some(child) = next_child { let child = &fragment.nodes[child]; child.serialize(fragment, serializer)?; next_child = child.next_sibling; } serializer.end_elem(data.name.clone())?; Ok(()) } NodeData::Document => { let mut next_child = self.first_child; while let Some(child) = next_child { let child = &fragment.nodes[child]; child.serialize(fragment, serializer)?; next_child = child.next_sibling; } Ok(()) } NodeData::Text(text) => serializer.write_text(text), _ => Ok(()), } } } /// The data of a `Node`. #[derive(Debug)] pub enum NodeData { /// The root node of the `Fragment`. Document, /// A text node. Text(StrTendril), /// An HTML element (aka a tag). Element(ElementData), /// Other types (comment, processing instruction, …). Other, } /// The data of an HTML element. #[derive(Debug)] pub struct ElementData { /// The qualified name of the element. pub name: QualName, /// The attributes of the element. pub attrs: BTreeSet, } #[cfg(test)] mod tests { use super::Fragment; #[test] fn sanity() { let html = "\

Title

\
\

This is some text

\
\ "; assert_eq!(Fragment::parse_html(html).to_string(), html); assert_eq!(Fragment::parse_html("").to_string(), ""); } } ruma-common-0.10.5/src/events/room/message/sanitize/html_sanitizer.rs000064400000000000000000000462221046102023000240720ustar 00000000000000use html5ever::{tendril::StrTendril, Attribute}; use phf::{phf_map, phf_set, Map, Set}; use wildmatch::WildMatch; use super::{ html_fragment::{ElementData, Fragment, NodeData}, HtmlSanitizerMode, RemoveReplyFallback, }; /// A sanitizer to filter [HTML tags and attributes] according to the Matrix specification. /// /// [HTML tags and attributes]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes #[derive(Debug, Clone)] pub struct HtmlSanitizer { /// The mode of the HTML sanitizer. mode: HtmlSanitizerMode, /// Whether to filter HTML tags and attributes. /// /// If this is `true`, tags and attributes that do not match the lists will be removed, but /// the tags' children will still be present in the output. /// /// If this is `false`, all the tags and attributes are allowed. filter_tags_attributes: bool, /// Whether to remove replies. /// /// If this is `true`, the rich reply fallback will be removed. /// /// If this is `false`, the rich reply tag will be allowed. remove_replies: bool, } impl HtmlSanitizer { /// Constructs a `HTMLSanitizer` that will filter the tags and attributes according to the given /// mode. /// /// It can also optionally remove the [rich reply fallback]. /// /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies pub fn new(mode: HtmlSanitizerMode, remove_reply_fallback: RemoveReplyFallback) -> Self { Self { mode, filter_tags_attributes: true, remove_replies: remove_reply_fallback == RemoveReplyFallback::Yes, } } /// Constructs a `HTMLSanitizer` instance that only removes the [rich reply fallback]. /// /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies pub fn reply_fallback_remover() -> Self { Self { mode: HtmlSanitizerMode::Strict, filter_tags_attributes: false, remove_replies: true, } } /// Clean the given HTML string with this sanitizer. pub fn clean(&self, html: &str) -> String { let mut fragment = Fragment::parse_html(html); let root = fragment.nodes[0].first_child.unwrap(); let mut next_child = fragment.nodes[root].first_child; while let Some(child) = next_child { next_child = fragment.nodes[child].next_sibling; self.clean_node(&mut fragment, child, 0); } fragment.to_string() } fn clean_node(&self, fragment: &mut Fragment, node_id: usize, depth: u32) { let action = self.node_action(fragment, node_id, depth); if action != NodeAction::Remove { let mut next_child = fragment.nodes[node_id].first_child; while let Some(child) = next_child { next_child = fragment.nodes[child].next_sibling; if action == NodeAction::Ignore { fragment.insert_before(node_id, child); } self.clean_node(fragment, child, depth + 1); } } if matches!(action, NodeAction::Ignore | NodeAction::Remove) { fragment.detach(node_id); } else if self.filter_tags_attributes { if let Some(data) = fragment.nodes[node_id].as_element_mut() { self.clean_element_attributes(data); } } } fn node_action(&self, fragment: &Fragment, node_id: usize, depth: u32) -> NodeAction { match &fragment.nodes[node_id].data { NodeData::Element(ElementData { name, attrs, .. }) => { let tag: &str = &name.local; if (self.remove_replies && tag == RICH_REPLY_TAG) || (self.filter_tags_attributes && depth >= MAX_DEPTH_STRICT) { NodeAction::Remove } else if self.filter_tags_attributes && (!ALLOWED_TAGS_WITHOUT_REPLY_STRICT.contains(tag) && tag != RICH_REPLY_TAG) { NodeAction::Ignore } else if self.filter_tags_attributes { let allowed_schemes = if self.mode == HtmlSanitizerMode::Strict { &ALLOWED_SCHEMES_STRICT } else { &ALLOWED_SCHEMES_COMPAT }; for attr in attrs.iter() { let value = &attr.value; let attr: &str = &attr.name.local; // Check if there is a (tag, attr) tuple entry. if let Some(schemes) = allowed_schemes.get(&*format!("{tag}:{attr}")) { // Check if the scheme is allowed. if !schemes .iter() .any(|scheme| value.starts_with(&format!("{scheme}:"))) { return NodeAction::Ignore; } } } NodeAction::None } else { NodeAction::None } } NodeData::Text(_) => NodeAction::None, _ => NodeAction::Remove, } } fn clean_element_attributes(&self, data: &mut ElementData) { let ElementData { name, attrs } = data; let tag: &str = &name.local; let actions: Vec<_> = attrs .iter() .filter_map(|attr| { let value = &attr.value; let name: &str = &attr.name.local; if ALLOWED_ATTRIBUTES_STRICT.get(tag).filter(|attrs| attrs.contains(name)).is_none() { return Some(AttributeAction::Remove(attr.to_owned())); } if name == "class" { if let Some(classes) = ALLOWED_CLASSES_STRICT.get(tag) { let mut changed = false; let attr_classes = value.split_whitespace().filter(|attr_class| { for class in classes.iter() { if WildMatch::new(class).matches(attr_class) { return true; } } changed = true; false }); let folded_classes = attr_classes.fold(String::new(), |mut a, b| { a.reserve(b.len() + 1); a.push_str(b); a.push('\n'); a }); let final_classes = folded_classes.trim_end(); if changed { if final_classes.is_empty() { return Some(AttributeAction::Remove(attr.to_owned())); } else { return Some(AttributeAction::ReplaceValue( attr.to_owned(), final_classes.to_owned().into(), )); } } } } None }) .collect(); for action in actions { match action { AttributeAction::ReplaceValue(attr, value) => { if let Some(mut attr) = attrs.take(&attr) { attr.value = value; attrs.insert(attr); } } AttributeAction::Remove(attr) => { attrs.remove(&attr); } } } } } /// The possible actions to apply to an element node. #[derive(Debug, PartialEq, Eq)] enum NodeAction { /// Don't do anything. None, /// Remove the element but keep its children. Ignore, /// Remove the element and its children. Remove, } /// The possible actions to apply to an element node. #[derive(Debug)] enum AttributeAction { /// Replace the value of the attribute. ReplaceValue(Attribute, StrTendril), /// Remove the element and its children. Remove(Attribute), } /// List of HTML tags allowed in the Matrix specification, without the rich reply fallback tag. static ALLOWED_TAGS_WITHOUT_REPLY_STRICT: Set<&str> = phf_set! { "font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div", "table", "thead", "tbody", "tr", "th", "td", "caption", "pre", "span", "img", "details", "summary", }; /// The HTML tag name for a rich reply fallback. const RICH_REPLY_TAG: &str = "mx-reply"; /// Allowed attributes per HTML tag according to the Matrix specification. static ALLOWED_ATTRIBUTES_STRICT: Map<&str, &Set<&str>> = phf_map! { "font" => &ALLOWED_ATTRIBUTES_FONT_STRICT, "span" => &ALLOWED_ATTRIBUTES_SPAN_STRICT, "a" => &ALLOWED_ATTRIBUTES_A_STRICT, "img" => &ALLOWED_ATTRIBUTES_IMG_STRICT, "ol" => &ALLOWED_ATTRIBUTES_OL_STRICT, "code" => &ALLOWED_ATTRIBUTES_CODE_STRICT, }; static ALLOWED_ATTRIBUTES_FONT_STRICT: Set<&str> = phf_set! { "data-mx-bg-color", "data-mx-color", "color" }; static ALLOWED_ATTRIBUTES_SPAN_STRICT: Set<&str> = phf_set! { "data-mx-bg-color", "data-mx-color", "data-mx-spoiler" }; static ALLOWED_ATTRIBUTES_A_STRICT: Set<&str> = phf_set! { "name", "target", "href" }; static ALLOWED_ATTRIBUTES_IMG_STRICT: Set<&str> = phf_set! { "width", "height", "alt", "title", "src" }; static ALLOWED_ATTRIBUTES_OL_STRICT: Set<&str> = phf_set! { "start" }; static ALLOWED_ATTRIBUTES_CODE_STRICT: Set<&str> = phf_set! { "class" }; /// Allowed schemes of URIs per HTML tag and attribute tuple according to the Matrix specification. static ALLOWED_SCHEMES_STRICT: Map<&str, &Set<&str>> = phf_map! { "a:href" => &ALLOWED_SCHEMES_A_HREF_STRICT, "img:src" => &ALLOWED_SCHEMES_IMG_SRC_STRICT, }; static ALLOWED_SCHEMES_A_HREF_STRICT: Set<&str> = phf_set! { "http", "https", "ftp", "mailto", "magnet" }; static ALLOWED_SCHEMES_IMG_SRC_STRICT: Set<&str> = phf_set! { "mxc" }; /// Extra allowed schemes of URIs per HTML tag and attribute tuple. /// /// This is a convenience list to add schemes that can be encountered but are not listed in the /// Matrix specification. It consists of: /// /// * The `matrix` scheme for `a` tags (see [matrix-org/matrix-spec#1108]). /// /// To get a complete list, add these to `ALLOWED_SCHEMES_STRICT`. /// /// [matrix-org/matrix-spec#1108]: https://github.com/matrix-org/matrix-spec/issues/1108 static ALLOWED_SCHEMES_COMPAT: Map<&str, &Set<&str>> = phf_map! { "a:href" => &ALLOWED_SCHEMES_A_HREF_COMPAT, "img:src" => &ALLOWED_SCHEMES_IMG_SRC_STRICT, }; static ALLOWED_SCHEMES_A_HREF_COMPAT: Set<&str> = phf_set! { "http", "https", "ftp", "mailto", "magnet", "matrix" }; /// Allowed classes per HTML tag according to the Matrix specification. static ALLOWED_CLASSES_STRICT: Map<&str, &Set<&str>> = phf_map! { "code" => &ALLOWED_CLASSES_CODE_STRICT }; static ALLOWED_CLASSES_CODE_STRICT: Set<&str> = phf_set! { "language-*" }; /// Max depth of nested HTML tags allowed by the Matrix specification. const MAX_DEPTH_STRICT: u32 = 100; #[cfg(test)] mod tests { use super::{HtmlSanitizer, HtmlSanitizerMode, RemoveReplyFallback}; #[test] fn valid_input() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::Yes); let sanitized = sanitizer.clean( "\
  • This
  • has
  • no
  • tag
\

This is a paragraph with some color

\ \ <mx-reply>This is a fake reply</mx-reply>\ ", ); assert_eq!( sanitized, "\
  • This
  • has
  • no
  • tag
\

This is a paragraph with some color

\ \ <mx-reply>This is a fake reply</mx-reply>\ " ); } #[test] fn tags_remove() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This has no tag\

But this is inside a tag

\ ", ); assert_eq!( sanitized, "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This has no tag\

But this is inside a tag

\ " ); } #[test] fn tags_remove_without_reply() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::Yes); let sanitized = sanitizer.clean( "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This has no tag\

But this is inside a tag

\ ", ); assert_eq!( sanitized, "\ This has no tag\

But this is inside a tag

\ " ); } #[test] fn tags_remove_only_reply_fallback() { let sanitizer = HtmlSanitizer::reply_fallback_remover(); let sanitized = sanitizer.clean( "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This keeps its tag\

But this is inside a tag

\ ", ); assert_eq!( sanitized, "\ This keeps its tag\

But this is inside a tag

\ " ); } #[test] fn attrs_remove() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\

Title for important stuff

\

Look at me!

\ ", ); assert_eq!( sanitized, "\

Title for important stuff

\

Look at me!

\ " ); } #[test] fn img_remove_scheme() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\

Look at that picture:

\ \ ", ); assert_eq!( sanitized, "\

Look at that picture:

\ " ); } #[test] fn link_remove_scheme() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\

Go see my local website

\ ", ); assert_eq!( sanitized, "\

Go see my local website

\ " ); } #[test] fn link_compat_scheme() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\

Join my room

\

To talk about my cat

\ ", ); assert_eq!( sanitized, "\

Join my room

\

To talk about my cat

\ " ); let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Compat, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\

Join my room

\

To talk about my cat

\ ", ); assert_eq!( sanitized, "\

Join my room

\

To talk about my cat

\ " ); } #[test] fn class_remove() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let sanitized = sanitizer.clean( "\

                type StringList = Vec<String>;
            
\

What do you think of the name StringList?

\ ", ); assert_eq!( sanitized, "\

                type StringList = Vec<String>;
            
\

What do you think of the name StringList?

\ " ); } #[test] fn depth_remove() { let sanitizer = HtmlSanitizer::new(HtmlSanitizerMode::Strict, RemoveReplyFallback::No); let deeply_nested_html: String = std::iter::repeat("
") .take(100) .chain(Some( "I am in too deep!\ I should be fine.", )) .chain(std::iter::repeat("
").take(100)) .collect(); let sanitized = sanitizer.clean(&deeply_nested_html); assert!(sanitized.contains("I should be fine.")); assert!(!sanitized.contains("I am in too deep!")); } } ruma-common-0.10.5/src/events/room/message/sanitize.rs000064400000000000000000000151341046102023000210340ustar 00000000000000//! Convenience methods and types to sanitize text messages. #[cfg(feature = "unstable-sanitize")] mod html_fragment; #[cfg(feature = "unstable-sanitize")] mod html_sanitizer; #[cfg(feature = "unstable-sanitize")] use html_sanitizer::HtmlSanitizer; /// Sanitize the given HTML string. /// /// This removes the [tags and attributes] that are not listed in the Matrix specification. /// /// It can also optionally remove the [rich reply fallback]. /// /// [tags and attributes]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies #[cfg(feature = "unstable-sanitize")] pub fn sanitize_html( s: &str, mode: HtmlSanitizerMode, remove_reply_fallback: RemoveReplyFallback, ) -> String { let sanitizer = HtmlSanitizer::new(mode, remove_reply_fallback); sanitizer.clean(s) } /// What HTML [tags and attributes] should be kept by the sanitizer. /// /// [tags and attributes]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes #[cfg(feature = "unstable-sanitize")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum HtmlSanitizerMode { /// Keep only the tags and attributes listed in the Matrix specification. Strict, /// Like `Strict` mode, with additional tags and attributes that are not yet included in /// the spec, but are reasonable to keep. Compat, } /// Whether to remove the [rich reply fallback] while sanitizing. /// /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies #[cfg(feature = "unstable-sanitize")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum RemoveReplyFallback { /// Remove the rich reply fallback. Yes, /// Don't remove the rich reply fallback. No, } /// Remove the [rich reply fallback] of the given HTML string. /// /// Due to the fact that the HTML is parsed, note that malformed HTML and comments will be stripped /// from the output. /// /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies #[cfg(feature = "unstable-sanitize")] pub fn remove_html_reply_fallback(s: &str) -> String { let sanitizer = HtmlSanitizer::reply_fallback_remover(); sanitizer.clean(s) } /// Remove the [rich reply fallback] of the given plain text string. /// /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies pub fn remove_plain_reply_fallback(mut s: &str) -> &str { while s.starts_with("> ") { if let Some((_line, rest)) = s.split_once('\n') { s = rest; } else { return ""; } } s } #[cfg(test)] mod tests { use super::remove_plain_reply_fallback; #[cfg(feature = "unstable-sanitize")] use super::{ remove_html_reply_fallback, sanitize_html, HtmlSanitizerMode, RemoveReplyFallback, }; #[test] #[cfg(feature = "unstable-sanitize")] fn sanitize() { let sanitized = sanitize_html( "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This has no tag\

But this is inside a tag

\ ", HtmlSanitizerMode::Strict, RemoveReplyFallback::No, ); assert_eq!( sanitized, "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This has no tag\

But this is inside a tag

\ " ); } #[test] #[cfg(feature = "unstable-sanitize")] fn sanitize_without_reply() { let sanitized = sanitize_html( "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This has no tag\

But this is inside a tag

\ ", HtmlSanitizerMode::Strict, RemoveReplyFallback::Yes, ); assert_eq!( sanitized, "\ This has no tag\

But this is inside a tag

\ " ); } #[test] #[cfg(feature = "unstable-sanitize")] fn remove_html_reply() { let without_reply = remove_html_reply_fallback( "\ \
\ In reply to \ @alice:example.com\
\ Previous message\
\
\ This keeps its tag\

But this is inside a tag

\ ", ); assert_eq!( without_reply, "\ This keeps its tag\

But this is inside a tag

\ " ); } #[test] fn remove_plain_reply() { assert_eq!( remove_plain_reply_fallback("No reply here\nJust a simple message"), "No reply here\nJust a simple message" ); assert_eq!( remove_plain_reply_fallback( "> <@user:notareal.hs> Replied to on\n\ > two lines\n\ This is my reply" ), "This is my reply" ); assert_eq!(remove_plain_reply_fallback("\n> Not on first line"), "\n> Not on first line"); assert_eq!( remove_plain_reply_fallback("> <@user:notareal.hs> Previous message\n\n> New quote"), "\n> New quote" ); } } ruma-common-0.10.5/src/events/room/message/server_notice.rs000064400000000000000000000044621046102023000220570ustar 00000000000000use serde::{Deserialize, Serialize}; use crate::{serde::StringEnum, PrivOwnedStr}; /// The payload for a server notice message. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.server_notice")] pub struct ServerNoticeMessageEventContent { /// A human-readable description of the notice. pub body: String, /// The type of notice being represented. pub server_notice_type: ServerNoticeType, /// A URI giving a contact method for the server administrator. /// /// Required if the notice type is `m.server_notice.usage_limit_reached`. #[serde(skip_serializing_if = "Option::is_none")] pub admin_contact: Option, /// The kind of usage limit the server has exceeded. /// /// Required if the notice type is `m.server_notice.usage_limit_reached`. #[serde(skip_serializing_if = "Option::is_none")] pub limit_type: Option, } impl ServerNoticeMessageEventContent { /// Creates a new `ServerNoticeMessageEventContent` with the given body and notice type. pub fn new(body: String, server_notice_type: ServerNoticeType) -> Self { Self { body, server_notice_type, admin_contact: None, limit_type: None } } } /// Types of server notices. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum ServerNoticeType { /// The server has exceeded some limit which requires the server administrator to intervene. #[ruma_enum(rename = "m.server_notice.usage_limit_reached")] UsageLimitReached, #[doc(hidden)] _Custom(PrivOwnedStr), } /// Types of usage limits. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum LimitType { /// The server's number of active users in the last 30 days has exceeded the maximum. /// /// New connections are being refused by the server. What defines "active" is left as an /// implementation detail, however servers are encouraged to treat syncing users as "active". MonthlyActiveUser, #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/events/room/message/text.rs000064400000000000000000000054171046102023000201750ustar 00000000000000use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc1767")] use crate::events::message::MessageContent; use super::FormattedBody; /// The payload for a text message. /// /// With the `unstable-msc1767` feature, this type contains the transitional format of /// [`MessageEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`MessageEventContent`]: crate::events::message::MessageEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.text")] pub struct TextMessageEventContent { /// The body of the message. pub body: String, /// Formatted form of the message `body`. #[serde(flatten)] pub formatted: Option, /// Extensible-event representation of the message. /// /// If present, this should be preferred over the other fields. #[cfg(feature = "unstable-msc1767")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, } impl TextMessageEventContent { /// A convenience constructor to create a plain text message. pub fn plain(body: impl Into) -> Self { let body = body.into(); Self { #[cfg(feature = "unstable-msc1767")] message: Some(MessageContent::plain(body.clone())), body, formatted: None, } } /// A convenience constructor to create an HTML message. pub fn html(body: impl Into, html_body: impl Into) -> Self { let body = body.into(); let html_body = html_body.into(); Self { #[cfg(feature = "unstable-msc1767")] message: Some(MessageContent::html(body.clone(), html_body.clone())), body, formatted: Some(FormattedBody::html(html_body)), } } /// A convenience constructor to create a Markdown message. /// /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain /// text message. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { if let Some(formatted) = FormattedBody::markdown(&body) { Self::html(body, formatted.body) } else { Self::plain(body) } } } #[cfg(feature = "unstable-msc1767")] impl From for TextMessageEventContent { fn from(message: MessageContent) -> Self { let body = if let Some(body) = message.find_plain() { body } else { &message[0].body }; let formatted = message.find_html().map(FormattedBody::html); Self { body: body.to_owned(), formatted, message: Some(message) } } } ruma-common-0.10.5/src/events/room/message/video.rs000064400000000000000000000235541046102023000203210ustar 00000000000000use std::time::Duration; use js_int::UInt; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3553")] use crate::events::{ file::{FileContent, FileContentInfo}, image::ThumbnailContent, message::MessageContent, video::VideoContent, }; use crate::{ events::room::{EncryptedFile, MediaSource, ThumbnailInfo}, OwnedMxcUri, }; /// The payload for a video message. /// /// With the `unstable-msc3553` feature, this type contains the transitional format of /// [`VideoEventContent`]. See the documentation of the [`message`] module for more information. /// /// [`VideoEventContent`]: crate::events::video::VideoEventContent /// [`message`]: crate::events::message #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "msgtype", rename = "m.video")] #[cfg_attr( feature = "unstable-msc3553", serde(from = "super::content_serde::VideoMessageEventContentDeHelper") )] pub struct VideoMessageEventContent { /// A description of the video, e.g. "Gangnam Style", or some kind of content description for /// accessibility, e.g. "video attachment". pub body: String, /// The source of the video clip. #[serde(flatten)] pub source: MediaSource, /// Metadata about the video clip referred to in `source`. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option>, /// Extensible-event text representation of the message. /// /// If present, this should be preferred over the `body` field. #[cfg(feature = "unstable-msc3553")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, /// Extensible-event file content of the message. /// /// If present, this should be preferred over the `source` and `info` fields. #[cfg(feature = "unstable-msc3553")] #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] pub file: Option, /// Extensible-event video info of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3553")] #[serde(rename = "org.matrix.msc1767.video", skip_serializing_if = "Option::is_none")] pub video: Option>, /// Extensible-event thumbnails of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3553")] #[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")] pub thumbnail: Option>, /// Extensible-event captions of the message. #[cfg(feature = "unstable-msc3553")] #[serde( rename = "org.matrix.msc1767.caption", with = "crate::events::message::content_serde::as_vec", default, skip_serializing_if = "Option::is_none" )] pub caption: Option, } impl VideoMessageEventContent { /// Creates a new non-encrypted `VideoMessageEventContent` with the given body, url and /// optional extra info. pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> Self { Self { #[cfg(feature = "unstable-msc3553")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3553")] file: Some(FileContent::plain( url.clone(), info.as_deref().and_then(|info| { FileContentInfo::from_room_message_content( None, info.mimetype.to_owned(), info.size.to_owned(), ) .map(Box::new) }), )), #[cfg(feature = "unstable-msc3553")] video: Some(Box::new( info.as_deref() .map(|info| { VideoContent::from_room_message_content( info.height, info.width, info.duration, ) }) .unwrap_or_default(), )), #[cfg(feature = "unstable-msc3553")] thumbnail: info .as_deref() .and_then(|info| { ThumbnailContent::from_room_message_content( info.thumbnail_source.to_owned(), info.thumbnail_info.to_owned(), ) }) .map(|thumbnail| vec![thumbnail]), #[cfg(feature = "unstable-msc3553")] caption: None, body, source: MediaSource::Plain(url), info, } } /// Creates a new encrypted `VideoMessageEventContent` with the given body and encrypted /// file. pub fn encrypted(body: String, file: EncryptedFile) -> Self { Self { #[cfg(feature = "unstable-msc3553")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3553")] file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)), #[cfg(feature = "unstable-msc3553")] video: Some(Box::new(VideoContent::default())), #[cfg(feature = "unstable-msc3553")] thumbnail: None, #[cfg(feature = "unstable-msc3553")] caption: None, body, source: MediaSource::Encrypted(Box::new(file)), info: None, } } /// Create a new `VideoMessageEventContent` with the given message, file info, video info, /// thumbnails and captions. #[cfg(feature = "unstable-msc3553")] pub(crate) fn from_extensible_content( message: MessageContent, file: FileContent, video: Box, thumbnail: Vec, caption: Option, ) -> Self { let body = if let Some(body) = message.find_plain() { body.to_owned() } else { message[0].body.clone() }; let source = (&file).into(); let info = VideoInfo::from_extensible_content(file.info.as_deref(), &video, &thumbnail) .map(Box::new); let thumbnail = if thumbnail.is_empty() { None } else { Some(thumbnail) }; Self { message: Some(message), file: Some(file), video: Some(video), thumbnail, caption, body, source, info, } } } /// Metadata about a video. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct VideoInfo { /// The duration of the video in milliseconds. #[serde( with = "crate::serde::duration::opt_ms", default, skip_serializing_if = "Option::is_none" )] pub duration: Option, /// The height of the video in pixels. #[serde(rename = "h", skip_serializing_if = "Option::is_none")] pub height: Option, /// The width of the video in pixels. #[serde(rename = "w", skip_serializing_if = "Option::is_none")] pub width: Option, /// The mimetype of the video, e.g. "video/mp4". #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The size of the video in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, /// Metadata about the image referred to in `thumbnail_source`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option>, /// The source of the thumbnail of the video clip. #[serde( flatten, with = "crate::events::room::thumbnail_source_serde", skip_serializing_if = "Option::is_none" )] pub thumbnail_source: Option, /// The [BlurHash](https://blurha.sh) for this video. /// /// This uses the unstable prefix in /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). #[cfg(feature = "unstable-msc2448")] #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")] pub blurhash: Option, } impl VideoInfo { /// Creates an empty `VideoInfo`. pub fn new() -> Self { Self::default() } /// Create a `VideoInfo` from the given file info, video info and thumbnail. /// /// Returns `None` if the `VideoInfo` would be empty. #[cfg(feature = "unstable-msc3553")] fn from_extensible_content( file_info: Option<&FileContentInfo>, video: &VideoContent, thumbnail: &[ThumbnailContent], ) -> Option { if file_info.is_none() && video.is_empty() && thumbnail.is_empty() { None } else { let (mimetype, size) = file_info .map(|info| (info.mimetype.to_owned(), info.size.to_owned())) .unwrap_or_default(); let VideoContent { duration, height, width } = video.to_owned(); let (thumbnail_source, thumbnail_info) = thumbnail .get(0) .map(|thumbnail| { let source = (&thumbnail.file).into(); let info = ThumbnailInfo::from_extensible_content( thumbnail.file.info.as_deref(), thumbnail.image.as_deref(), ) .map(Box::new); (Some(source), info) }) .unwrap_or_default(); Some(Self { duration, height, width, mimetype, size, thumbnail_info, thumbnail_source, #[cfg(feature = "unstable-msc2448")] blurhash: None, }) } } } ruma-common-0.10.5/src/events/room/message.rs000064400000000000000000000664621046102023000172200ustar 00000000000000//! Types for the [`m.room.message`] event. //! //! [`m.room.message`]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage use std::{borrow::Cow, fmt}; use ruma_macros::EventContent; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value as JsonValue; use crate::{ serde::{JsonObject, StringEnum}, OwnedEventId, PrivOwnedStr, }; mod audio; mod content_serde; mod emote; mod file; mod image; mod key_verification_request; mod location; mod notice; mod relation_serde; mod reply; pub mod sanitize; mod server_notice; mod text; mod video; pub use audio::{AudioInfo, AudioMessageEventContent}; pub use emote::EmoteMessageEventContent; pub use file::{FileInfo, FileMessageEventContent}; pub use image::ImageMessageEventContent; pub use key_verification_request::KeyVerificationRequestEventContent; pub use location::{LocationInfo, LocationMessageEventContent}; pub use notice::NoticeMessageEventContent; #[cfg(feature = "unstable-sanitize")] use sanitize::{ remove_plain_reply_fallback, sanitize_html, HtmlSanitizerMode, RemoveReplyFallback, }; pub use server_notice::{LimitType, ServerNoticeMessageEventContent, ServerNoticeType}; pub use text::TextMessageEventContent; pub use video::{VideoInfo, VideoMessageEventContent}; /// The content of an `m.room.message` event. /// /// This event is used when sending messages in a room. /// /// Messages are not limited to be text. #[derive(Clone, Debug, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.message", kind = MessageLike)] pub struct RoomMessageEventContent { /// A key which identifies the type of message being sent. /// /// This also holds the specific content of each message. #[serde(flatten)] pub msgtype: MessageType, /// Information about related messages for [rich replies]. /// /// [rich replies]: https://spec.matrix.org/v1.2/client-server-api/#rich-replies #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl RoomMessageEventContent { /// Create a `RoomMessageEventContent` with the given `MessageType`. pub fn new(msgtype: MessageType) -> Self { Self { msgtype, relates_to: None } } /// A constructor to create a plain text message. pub fn text_plain(body: impl Into) -> Self { Self::new(MessageType::Text(TextMessageEventContent::plain(body))) } /// A constructor to create an html message. pub fn text_html(body: impl Into, html_body: impl Into) -> Self { Self::new(MessageType::Text(TextMessageEventContent::html(body, html_body))) } /// A constructor to create a markdown message. #[cfg(feature = "markdown")] pub fn text_markdown(body: impl AsRef + Into) -> Self { Self::new(MessageType::Text(TextMessageEventContent::markdown(body))) } /// A constructor to create a plain text notice. pub fn notice_plain(body: impl Into) -> Self { Self::new(MessageType::Notice(NoticeMessageEventContent::plain(body))) } /// A constructor to create an html notice. pub fn notice_html(body: impl Into, html_body: impl Into) -> Self { Self::new(MessageType::Notice(NoticeMessageEventContent::html(body, html_body))) } /// A constructor to create a markdown notice. #[cfg(feature = "markdown")] pub fn notice_markdown(body: impl AsRef + Into) -> Self { Self::new(MessageType::Notice(NoticeMessageEventContent::markdown(body))) } /// Turns `self` into a reply to the given message. /// /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] /// /// # Panics /// /// Panics if `self` has a `formatted_body` with a format other than HTML. #[track_caller] pub fn make_reply_to(mut self, original_message: &OriginalRoomMessageEvent) -> Self { let empty_formatted_body = || FormattedBody::html(String::new()); let (body, formatted) = { match &mut self.msgtype { MessageType::Emote(m) => { (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body))) } MessageType::Notice(m) => { (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body))) } MessageType::Text(m) => { (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body))) } MessageType::Audio(m) => (&mut m.body, None), MessageType::File(m) => (&mut m.body, None), MessageType::Image(m) => (&mut m.body, None), MessageType::Location(m) => (&mut m.body, None), MessageType::ServerNotice(m) => (&mut m.body, None), MessageType::Video(m) => (&mut m.body, None), MessageType::VerificationRequest(m) => (&mut m.body, None), MessageType::_Custom(m) => (&mut m.body, None), } }; if let Some(f) = formatted { assert_eq!( f.format, MessageFormat::Html, "make_reply_to can't handle non-HTML formatted messages" ); let formatted_body = &mut f.body; (*body, *formatted_body) = reply::plain_and_formatted_reply_body( body.as_str(), (!formatted_body.is_empty()).then(|| formatted_body.as_str()), original_message, ); } self.relates_to = Some(Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() }, }); self } /// Creates a plain text reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[deprecated = "\ use [`Self::text_plain`](#method.text_plain)`(reply).`\ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ "] pub fn text_reply_plain( reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { Self::text_plain(reply.to_string()).make_reply_to(original_message) } /// Creates a html text reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[deprecated = "\ use [`Self::text_html`](#method.text_html)`(reply, html_reply).`\ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ "] pub fn text_reply_html( reply: impl fmt::Display, html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { Self::text_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message) } /// Creates a plain text notice reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[deprecated = "\ use [`Self::notice_plain`](#method.notice_plain)`(reply).`\ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ "] pub fn notice_reply_plain( reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { Self::notice_plain(reply.to_string()).make_reply_to(original_message) } /// Creates a html text notice reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[deprecated = "\ use [`Self::notice_html`](#method.notice_html)`(reply, html_reply).`\ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ "] pub fn notice_reply_html( reply: impl fmt::Display, html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { Self::notice_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message) } /// Create a new reply with the given message and optionally forwards the [`Relation::Thread`]. /// /// If `message` is a text, an emote or a notice message, it is modified to include the rich /// reply fallback. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[cfg(feature = "unstable-msc3440")] pub fn reply( message: MessageType, original_message: &OriginalRoomMessageEvent, forward_thread: ForwardThread, ) -> Self { let make_reply = |body, formatted: Option| { reply::plain_and_formatted_reply_body(body, formatted.map(|f| f.body), original_message) }; let msgtype = match message { MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Text(TextMessageEventContent::html(body, html_body)) } MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Emote(EmoteMessageEventContent::html(body, html_body)) } MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Notice(NoticeMessageEventContent::html(body, html_body)) } _ => message, }; let relates_to = if let Some(Relation::Thread(Thread { event_id, .. })) = original_message .content .relates_to .as_ref() .filter(|_| forward_thread == ForwardThread::Yes) { Relation::Thread(Thread::reply(event_id.clone(), original_message.event_id.clone())) } else { Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, } }; Self { msgtype, relates_to: Some(relates_to) } } /// Create a new message for a thread that is optionally a reply. /// /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, a message for the same /// thread is created. If it doesn't, a new thread with `previous_message` as the root is /// created. /// /// If `message` is a text, an emote or a notice message, and this is a reply in the thread, it /// is modified to include the rich reply fallback. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[cfg(feature = "unstable-msc3440")] pub fn for_thread( message: MessageType, previous_message: &OriginalRoomMessageEvent, is_reply: ReplyInThread, ) -> Self { let make_reply = |body, formatted: Option| { reply::plain_and_formatted_reply_body(body, formatted.map(|f| f.body), previous_message) }; let msgtype = if is_reply == ReplyInThread::Yes { // If this is a real reply, add the rich reply fallback. match message { MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Text(TextMessageEventContent::html(body, html_body)) } MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Emote(EmoteMessageEventContent::html(body, html_body)) } MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Notice(NoticeMessageEventContent::html(body, html_body)) } _ => message, } } else { message }; let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) = &previous_message.content.relates_to { event_id.clone() } else { previous_message.event_id.clone() }; Self { msgtype, relates_to: Some(Relation::Thread(Thread { event_id: thread_root, in_reply_to: InReplyTo { event_id: previous_message.event_id.clone() }, is_falling_back: is_reply == ReplyInThread::No, })), } } /// Returns a reference to the `msgtype` string. /// /// If you want to access the message type-specific data rather than the message type itself, /// use the `msgtype` *field*, not this method. pub fn msgtype(&self) -> &str { self.msgtype.msgtype() } /// Return a reference to the message body. pub fn body(&self) -> &str { self.msgtype.body() } /// Sanitize this message. /// /// If this message contains HTML, this removes the [tags and attributes] that are not listed in /// the Matrix specification. /// /// It can also optionally remove the [rich reply fallback] from the plain text and HTML /// message. /// /// This method is only effective on text, notice and emote messages. /// /// [tags and attributes]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies #[cfg(feature = "unstable-sanitize")] pub fn sanitize( &mut self, mode: HtmlSanitizerMode, remove_reply_fallback: RemoveReplyFallback, ) { if let MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) | MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) | MessageType::Text(TextMessageEventContent { body, formatted, .. }) = &mut self.msgtype { if let Some(formatted) = formatted { formatted.sanitize_html(mode, remove_reply_fallback); } if remove_reply_fallback == RemoveReplyFallback::Yes && matches!(self.relates_to, Some(Relation::Reply { .. })) { *body = remove_plain_reply_fallback(body).to_owned(); } } } } /// Whether or not to forward a [`Relation::Thread`] when sending a reply. #[cfg(feature = "unstable-msc3440")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum ForwardThread { /// The thread relation in the original message is forwarded if it exists. /// /// This should be set if your client doesn't support threads (see [MSC3440]). /// /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440 Yes, /// Create a reply in the main conversation even if the original message is in a thread. /// /// This should be used if you client supports threads and you explicitly want that behavior. No, } /// Whether or not the message is a reply inside a thread. #[cfg(feature = "unstable-msc3440")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum ReplyInThread { /// This is a reply. /// /// Create a proper reply _in_ the thread. Yes, /// This is not a reply. /// /// Create a regular message in the thread, with a reply fallback, according to [MSC3440]. /// /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440 No, } /// The content that is specific to each message type variant. #[derive(Clone, Debug, Serialize)] #[serde(untagged)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum MessageType { /// An audio message. Audio(AudioMessageEventContent), /// An emote message. Emote(EmoteMessageEventContent), /// A file message. File(FileMessageEventContent), /// An image message. Image(ImageMessageEventContent), /// A location message. Location(LocationMessageEventContent), /// A notice message. Notice(NoticeMessageEventContent), /// A server notice message. ServerNotice(ServerNoticeMessageEventContent), /// A text message. Text(TextMessageEventContent), /// A video message. Video(VideoMessageEventContent), /// A request to initiate a key verification. VerificationRequest(KeyVerificationRequestEventContent), /// A custom message. #[doc(hidden)] _Custom(CustomEventContent), } impl MessageType { /// Creates a new `MessageType`. /// /// The `msgtype` and `body` are required fields as defined by [the `m.room.message` spec](https://spec.matrix.org/v1.2/client-server-api/#mroommessage). /// Additionally it's possible to add arbitrary key/value pairs to the event content for custom /// events through the `data` map. /// /// Prefer to use the public variants of `MessageType` where possible; this constructor is meant /// be used for unsupported message types only and does not allow setting arbitrary data for /// supported ones. /// /// # Errors /// /// Returns an error if the `msgtype` is known and serialization of `data` to the corresponding /// `MessageType` variant fails. pub fn new(msgtype: &str, body: String, data: JsonObject) -> serde_json::Result { fn deserialize_variant( body: String, mut obj: JsonObject, ) -> serde_json::Result { obj.insert("body".into(), body.into()); serde_json::from_value(JsonValue::Object(obj)) } Ok(match msgtype { "m.audio" => Self::Audio(deserialize_variant(body, data)?), "m.emote" => Self::Emote(deserialize_variant(body, data)?), "m.file" => Self::File(deserialize_variant(body, data)?), "m.image" => Self::Image(deserialize_variant(body, data)?), "m.location" => Self::Location(deserialize_variant(body, data)?), "m.notice" => Self::Notice(deserialize_variant(body, data)?), "m.server_notice" => Self::ServerNotice(deserialize_variant(body, data)?), "m.text" => Self::Text(deserialize_variant(body, data)?), "m.video" => Self::Video(deserialize_variant(body, data)?), "m.key.verification.request" => { Self::VerificationRequest(deserialize_variant(body, data)?) } _ => Self::_Custom(CustomEventContent { msgtype: msgtype.to_owned(), body, data }), }) } /// Returns a reference to the `msgtype` string. pub fn msgtype(&self) -> &str { match self { Self::Audio(_) => "m.audio", Self::Emote(_) => "m.emote", Self::File(_) => "m.file", Self::Image(_) => "m.image", Self::Location(_) => "m.location", Self::Notice(_) => "m.notice", Self::ServerNotice(_) => "m.server_notice", Self::Text(_) => "m.text", Self::Video(_) => "m.video", Self::VerificationRequest(_) => "m.key.verification.request", Self::_Custom(c) => &c.msgtype, } } /// Return a reference to the message body. pub fn body(&self) -> &str { match self { MessageType::Audio(m) => &m.body, MessageType::Emote(m) => &m.body, MessageType::File(m) => &m.body, MessageType::Image(m) => &m.body, MessageType::Location(m) => &m.body, MessageType::Notice(m) => &m.body, MessageType::ServerNotice(m) => &m.body, MessageType::Text(m) => &m.body, MessageType::Video(m) => &m.body, MessageType::VerificationRequest(m) => &m.body, MessageType::_Custom(m) => &m.body, } } /// Returns the associated data. /// /// The returned JSON object won't contain the `msgtype` and `body` fields, use /// [`.msgtype()`][Self::msgtype] / [`.body()`](Self::body) to access those. /// /// Prefer to use the public variants of `MessageType` where possible; this method is meant to /// be used for custom message types only. pub fn data(&self) -> Cow<'_, JsonObject> { fn serialize(obj: &T) -> JsonObject { match serde_json::to_value(obj).expect("message type serialization to succeed") { JsonValue::Object(mut obj) => { obj.remove("body"); obj } _ => panic!("all message types must serialize to objects"), } } match self { Self::Audio(d) => Cow::Owned(serialize(d)), Self::Emote(d) => Cow::Owned(serialize(d)), Self::File(d) => Cow::Owned(serialize(d)), Self::Image(d) => Cow::Owned(serialize(d)), Self::Location(d) => Cow::Owned(serialize(d)), Self::Notice(d) => Cow::Owned(serialize(d)), Self::ServerNotice(d) => Cow::Owned(serialize(d)), Self::Text(d) => Cow::Owned(serialize(d)), Self::Video(d) => Cow::Owned(serialize(d)), Self::VerificationRequest(d) => Cow::Owned(serialize(d)), Self::_Custom(c) => Cow::Borrowed(&c.data), } } } impl From for RoomMessageEventContent { fn from(msgtype: MessageType) -> Self { Self::new(msgtype) } } /// Message event relationship. #[derive(Clone, Debug)] #[allow(clippy::manual_non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum Relation { /// An `m.in_reply_to` relation indicating that the event is a reply to another event. Reply { /// Information about another message being replied to. in_reply_to: InReplyTo, }, /// An event that replaces another event. #[cfg(feature = "unstable-msc2676")] Replacement(Replacement), /// An event that belongs to a thread. #[cfg(feature = "unstable-msc3440")] Thread(Thread), #[doc(hidden)] _Custom, } /// Information about the event a "rich reply" is replying to. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct InReplyTo { /// The event being replied to. pub event_id: OwnedEventId, } impl InReplyTo { /// Creates a new `InReplyTo` with the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } /// The event this relation belongs to replaces another event. #[derive(Clone, Debug)] #[cfg(feature = "unstable-msc2676")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Replacement { /// The ID of the event being replaced. pub event_id: OwnedEventId, /// New content. pub new_content: Box, } #[cfg(feature = "unstable-msc2676")] impl Replacement { /// Creates a new `Replacement` with the given event ID and new content. pub fn new(event_id: OwnedEventId, new_content: Box) -> Self { Self { event_id, new_content } } } /// The content of a thread relation. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Thread { /// The ID of the root message in the thread. pub event_id: OwnedEventId, /// A reply relation. /// /// If this event is a reply and belongs to a thread, this points to the message that is being /// replied to, and `is_falling_back` must be set to `false`. /// /// If this event is not a reply, this is used as a fallback mechanism for clients that do not /// support threads. This should point to the latest message-like event in the thread and /// `is_falling_back` must be set to `true`. pub in_reply_to: InReplyTo, /// Whether the `m.in_reply_to` field is a fallback for older clients or a genuine reply in a /// thread. pub is_falling_back: bool, } #[cfg(feature = "unstable-msc3440")] impl Thread { /// Convenience method to create a regular `Thread` with the given event ID and latest /// message-like event ID. pub fn plain(event_id: OwnedEventId, latest_event_id: OwnedEventId) -> Self { Self { event_id, in_reply_to: InReplyTo::new(latest_event_id), is_falling_back: true } } /// Convenience method to create a reply `Thread` with the given event ID and replied-to event /// ID. pub fn reply(event_id: OwnedEventId, reply_to_event_id: OwnedEventId) -> Self { Self { event_id, in_reply_to: InReplyTo::new(reply_to_event_id), is_falling_back: false } } } /// The format for the formatted representation of a message body. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum MessageFormat { /// HTML. #[ruma_enum(rename = "org.matrix.custom.html")] Html, #[doc(hidden)] _Custom(PrivOwnedStr), } /// Common message event content fields for message types that have separate plain-text and /// formatted representations. #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct FormattedBody { /// The format used in the `formatted_body`. pub format: MessageFormat, /// The formatted version of the `body`. #[serde(rename = "formatted_body")] pub body: String, } impl FormattedBody { /// Creates a new HTML-formatted message body. pub fn html(body: impl Into) -> Self { Self { format: MessageFormat::Html, body: body.into() } } /// Creates a new HTML-formatted message body by parsing the Markdown in `body`. /// /// Returns `None` if no Markdown formatting was found. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef) -> Option { let body = body.as_ref(); let mut html_body = String::new(); pulldown_cmark::html::push_html(&mut html_body, pulldown_cmark::Parser::new(body)); (html_body != format!("

{}

\n", body)).then(|| Self::html(html_body)) } /// Sanitize this `FormattedBody` if its format is `MessageFormat::Html`. /// /// This removes any [tags and attributes] that are not listed in the Matrix specification. /// /// It can also optionally remove the [rich reply fallback]. /// /// Returns the sanitized HTML if the format is `MessageFormat::Html`. /// /// [tags and attributes]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes /// [rich reply fallback]: https://spec.matrix.org/v1.2/client-server-api/#fallbacks-for-rich-replies #[cfg(feature = "unstable-sanitize")] pub fn sanitize_html( &mut self, mode: HtmlSanitizerMode, remove_reply_fallback: RemoveReplyFallback, ) { if self.format == MessageFormat::Html { self.body = sanitize_html(&self.body, mode, remove_reply_fallback); } } } /// The payload for a custom message event. #[doc(hidden)] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CustomEventContent { /// A custom msgtype. msgtype: String, /// The message body. body: String, /// Remaining event content. #[serde(flatten)] data: JsonObject, } ruma-common-0.10.5/src/events/room/name.rs000064400000000000000000000151131046102023000164770ustar 00000000000000//! Types for the [`m.room.name`] event. //! //! [`m.room.name`]: https://spec.matrix.org/v1.2/client-server-api/#mroomname use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::events::EmptyStateKey; /// The content of an `m.room.name` event. /// /// The room name is a human-friendly string designed to be displayed to the end-user. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.name", kind = State, state_key_type = EmptyStateKey)] pub struct RoomNameEventContent { /// The name of the room. #[serde(default, deserialize_with = "crate::serde::empty_string_as_none")] pub name: Option, } impl RoomNameEventContent { /// Create a new `RoomNameEventContent` with the given name. pub fn new(name: Option) -> Self { let name = name.filter(|n| !n.is_empty()); Self { name } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use js_int::{int, uint}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::RoomNameEventContent; use crate::{ event_id, events::{EmptyStateKey, OriginalStateEvent, StateUnsigned}, room_id, serde::Raw, user_id, MilliSecondsSinceUnixEpoch, }; #[test] fn serialization_with_optional_fields_as_none() { let name_event = OriginalStateEvent { content: RoomNameEventContent { name: Some("The room name".to_owned()) }, event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!n8f893n9:example.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), state_key: EmptyStateKey, unsigned: StateUnsigned::default(), }; let actual = to_json_value(&name_event).unwrap(); let expected = json!({ "content": { "name": "The room name" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.name" }); assert_eq!(actual, expected); } #[test] fn serialization_with_all_fields() { let name_event = OriginalStateEvent { content: RoomNameEventContent { name: Some("The room name".to_owned()) }, event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!n8f893n9:example.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), state_key: EmptyStateKey, unsigned: StateUnsigned { age: Some(int!(100)), prev_content: Some(RoomNameEventContent { name: Some("The old name".to_owned()) }), ..StateUnsigned::default() }, }; let actual = to_json_value(&name_event).unwrap(); let expected = json!({ "content": { "name": "The room name" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.name", "unsigned": { "age": 100, "prev_content": { "name": "The old name" }, } }); assert_eq!(actual, expected); } #[test] fn absent_field_as_none() { let json_data = json!({ "content": {}, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.name" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .name, None ); } #[test] fn json_with_empty_name_creates_content_as_none() { let long_content_json = json!({ "name": "" }); let from_raw: Raw = from_json_value(long_content_json).unwrap(); assert_matches!(from_raw.deserialize().unwrap(), RoomNameEventContent { name: None }); } #[test] fn new_with_empty_name_creates_content_as_none() { assert_matches!( RoomNameEventContent::new(Some("".to_owned())), RoomNameEventContent { name: None } ); } #[test] fn null_field_as_none() { let json_data = json!({ "content": { "name": null }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.name" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .name, None ); } #[test] fn empty_string_as_none() { let json_data = json!({ "content": { "name": "" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.name" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .name, None ); } #[test] fn nonempty_field_as_some() { let name = "The room name".try_into().ok(); let json_data = json!({ "content": { "name": "The room name" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.name" }); assert_eq!( from_json_value::>(json_data) .unwrap() .content .name, name ); } } ruma-common-0.10.5/src/events/room/pinned_events.rs000064400000000000000000000052441046102023000204240ustar 00000000000000//! Types for the [`m.room.pinned_events`] event. //! //! [`m.room.pinned_events`]: https://spec.matrix.org/v1.2/client-server-api/#mroompinned_events use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{events::EmptyStateKey, OwnedEventId}; /// The content of an `m.room.pinned_events` event. /// /// Used to "pin" particular events in a room for other participants to review later. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.pinned_events", kind = State, state_key_type = EmptyStateKey)] pub struct RoomPinnedEventsEventContent { /// An ordered list of event IDs to pin. pub pinned: Vec, } impl RoomPinnedEventsEventContent { /// Creates a new `RoomPinnedEventsEventContent` with the given events. pub fn new(pinned: Vec) -> Self { Self { pinned } } } #[cfg(all(test, feature = "rand"))] mod tests { use super::RoomPinnedEventsEventContent; use crate::{ events::{EmptyStateKey, OriginalStateEvent, StateUnsigned}, server_name, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, }; #[test] fn serialization_deserialization() { let mut content: RoomPinnedEventsEventContent = RoomPinnedEventsEventContent { pinned: Vec::new() }; let server_name = server_name!("example.com"); content.pinned.push(EventId::new(server_name)); content.pinned.push(EventId::new(server_name)); let event = OriginalStateEvent { content: content.clone(), event_id: EventId::new(server_name), origin_server_ts: MilliSecondsSinceUnixEpoch(1_432_804_485_886_u64.try_into().unwrap()), room_id: RoomId::new(server_name), sender: UserId::new(server_name), state_key: EmptyStateKey, unsigned: StateUnsigned::default(), }; let serialized_event = serde_json::to_string(&event).unwrap(); let parsed_event: OriginalStateEvent = serde_json::from_str(&serialized_event).unwrap(); assert_eq!(parsed_event.event_id, event.event_id); assert_eq!(parsed_event.room_id, event.room_id); assert_eq!(parsed_event.sender, event.sender); assert_eq!(parsed_event.state_key, event.state_key); assert_eq!(parsed_event.origin_server_ts, event.origin_server_ts); assert_eq!(parsed_event.content.pinned, event.content.pinned); assert_eq!(parsed_event.content.pinned[0], content.pinned[0]); assert_eq!(parsed_event.content.pinned[1], content.pinned[1]); } } ruma-common-0.10.5/src/events/room/power_levels.rs000064400000000000000000000407031046102023000202700ustar 00000000000000//! Types for the [`m.room.power_levels`] event. //! //! [`m.room.power_levels`]: https://spec.matrix.org/v1.2/client-server-api/#mroompower_levels use std::{cmp::max, collections::BTreeMap}; use js_int::{int, Int}; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{ events::{EmptyStateKey, MessageLikeEventType, RoomEventType, StateEventType}, power_levels::{default_power_level, NotificationPowerLevels}, OwnedUserId, UserId, }; /// The content of an `m.room.power_levels` event. /// /// Defines the power levels (privileges) of users in the room. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.power_levels", kind = State, state_key_type = EmptyStateKey)] pub struct RoomPowerLevelsEventContent { /// The level required to ban a user. #[serde( default = "default_power_level", skip_serializing_if = "is_default_power_level", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] #[ruma_event(skip_redaction)] pub ban: Int, /// The level required to send specific event types. /// /// This is a mapping from event type to power level required. #[serde( default, skip_serializing_if = "BTreeMap::is_empty", deserialize_with = "crate::serde::btreemap_deserialize_v1_powerlevel_values" )] #[ruma_event(skip_redaction)] pub events: BTreeMap, /// The default level required to send message events. #[serde( default, skip_serializing_if = "crate::serde::is_default", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] #[ruma_event(skip_redaction)] pub events_default: Int, /// The level required to invite a user. #[serde( default, skip_serializing_if = "crate::serde::is_default", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] pub invite: Int, /// The level required to kick a user. #[serde( default = "default_power_level", skip_serializing_if = "is_default_power_level", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] #[ruma_event(skip_redaction)] pub kick: Int, /// The level required to redact an event. #[serde( default = "default_power_level", skip_serializing_if = "is_default_power_level", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] #[ruma_event(skip_redaction)] pub redact: Int, /// The default level required to send state events. #[serde( default = "default_power_level", skip_serializing_if = "is_default_power_level", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] #[ruma_event(skip_redaction)] pub state_default: Int, /// The power levels for specific users. /// /// This is a mapping from `user_id` to power level for that user. #[serde( default, skip_serializing_if = "BTreeMap::is_empty", deserialize_with = "crate::serde::btreemap_deserialize_v1_powerlevel_values" )] #[ruma_event(skip_redaction)] pub users: BTreeMap, /// The default power level for every user in the room. #[serde( default, skip_serializing_if = "crate::serde::is_default", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] #[ruma_event(skip_redaction)] pub users_default: Int, /// The power level requirements for specific notification types. /// /// This is a mapping from `key` to power level for that notifications key. #[serde(default, skip_serializing_if = "NotificationPowerLevels::is_default")] pub notifications: NotificationPowerLevels, } impl RoomPowerLevelsEventContent { /// Creates a new `RoomPowerLevelsEventContent` with all-default values. pub fn new() -> Self { // events_default, users_default and invite having a default of 0 while the others have a // default of 50 is not an oversight, these defaults are from the Matrix specification. Self { ban: default_power_level(), events: BTreeMap::new(), events_default: int!(0), invite: int!(0), kick: default_power_level(), redact: default_power_level(), state_default: default_power_level(), users: BTreeMap::new(), users_default: int!(0), notifications: NotificationPowerLevels::default(), } } } impl Default for RoomPowerLevelsEventContent { fn default() -> Self { Self::new() } } /// Used with `#[serde(skip_serializing_if)]` to omit default power levels. #[allow(clippy::trivially_copy_pass_by_ref)] fn is_default_power_level(l: &Int) -> bool { *l == int!(50) } impl RoomPowerLevelsEvent { /// Obtain the effective power levels, regardless of whether this event is redacted. pub fn power_levels(&self) -> RoomPowerLevels { match self { Self::Original(ev) => ev.content.clone().into(), Self::Redacted(ev) => ev.content.clone().into(), } } } impl SyncRoomPowerLevelsEvent { /// Obtain the effective power levels, regardless of whether this event is redacted. pub fn power_levels(&self) -> RoomPowerLevels { match self { Self::Original(ev) => ev.content.clone().into(), Self::Redacted(ev) => ev.content.clone().into(), } } } impl StrippedRoomPowerLevelsEvent { /// Obtain the effective power levels from this event. pub fn power_levels(&self) -> RoomPowerLevels { self.content.clone().into() } } /// The effective power levels of a room. /// /// This struct contains the same fields as [`RoomPowerLevelsEventContent`] and be created from that /// using a `From` trait implementation, but it is also implements /// `From<`[`RedactedRoomPowerLevelsEventContent`]`>`, so can be used when wanting to inspect the /// power levels of a room, regardless of whether the most recent power-levels event is redacted or /// not. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RoomPowerLevels { /// The level required to ban a user. pub ban: Int, /// The level required to send specific event types. /// /// This is a mapping from event type to power level required. pub events: BTreeMap, /// The default level required to send message events. pub events_default: Int, /// The level required to invite a user. pub invite: Int, /// The level required to kick a user. pub kick: Int, /// The level required to redact an event. pub redact: Int, /// The default level required to send state events. pub state_default: Int, /// The power levels for specific users. /// /// This is a mapping from `user_id` to power level for that user. pub users: BTreeMap, /// The default power level for every user in the room. pub users_default: Int, /// The power level requirements for specific notification types. /// /// This is a mapping from `key` to power level for that notifications key. pub notifications: NotificationPowerLevels, } impl RoomPowerLevels { /// Get the power level of a specific user. pub fn for_user(&self, user_id: &UserId) -> Int { self.users.get(user_id).map_or(self.users_default, |pl| *pl) } /// Whether the given user can do the given action based on the power levels. pub fn user_can_do(&self, user_id: &UserId, action: PowerLevelAction) -> bool { let user_pl = self.for_user(user_id); match action { PowerLevelAction::Ban => user_pl >= self.ban, PowerLevelAction::Invite => user_pl >= self.invite, PowerLevelAction::Kick => user_pl >= self.kick, PowerLevelAction::Redact => user_pl >= self.redact, PowerLevelAction::SendMessage(message_type) => { user_pl >= self .events .get(&message_type.into()) .map(ToOwned::to_owned) .unwrap_or(self.events_default) } PowerLevelAction::SendState(state_type) => { user_pl >= self .events .get(&state_type.into()) .map(ToOwned::to_owned) .unwrap_or(self.state_default) } PowerLevelAction::TriggerNotification(notification_type) => match notification_type { NotificationPowerLevelType::Room => user_pl >= self.notifications.room, }, } } /// Get the maximum power level of any user. pub fn max(&self) -> Int { self.users.values().fold(self.users_default, |max_pl, user_pl| max(max_pl, *user_pl)) } } impl From for RoomPowerLevels { fn from(c: RoomPowerLevelsEventContent) -> Self { Self { ban: c.ban, events: c.events, events_default: c.events_default, invite: c.invite, kick: c.kick, redact: c.redact, state_default: c.state_default, users: c.users, users_default: c.users_default, notifications: c.notifications, } } } impl From for RoomPowerLevels { fn from(c: RedactedRoomPowerLevelsEventContent) -> Self { Self { ban: c.ban, events: c.events, events_default: c.events_default, invite: int!(0), kick: c.kick, redact: c.redact, state_default: c.state_default, users: c.users, users_default: c.users_default, notifications: NotificationPowerLevels::default(), } } } impl From for RoomPowerLevelsEventContent { fn from(c: RoomPowerLevels) -> Self { Self { ban: c.ban, events: c.events, events_default: c.events_default, invite: c.invite, kick: c.kick, redact: c.redact, state_default: c.state_default, users: c.users, users_default: c.users_default, notifications: c.notifications, } } } /// The actions that can be limited by power levels. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum PowerLevelAction { /// Ban a user. Ban, /// Invite a user. Invite, /// Kick a user. Kick, /// Redact an event. Redact, /// Send a message-like event. SendMessage(MessageLikeEventType), /// Send a state event. SendState(StateEventType), /// Trigger a notification. TriggerNotification(NotificationPowerLevelType), } /// The notification types that can be limited by power levels. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum NotificationPowerLevelType { /// `@room` notifications. Room, } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assign::assign; use js_int::{int, uint}; use maplit::btreemap; use serde_json::{json, to_value as to_json_value}; use super::{default_power_level, NotificationPowerLevels, RoomPowerLevelsEventContent}; use crate::{ event_id, events::{EmptyStateKey, OriginalStateEvent, StateUnsigned}, room_id, user_id, MilliSecondsSinceUnixEpoch, }; #[test] fn serialization_with_optional_fields_as_none() { let default = default_power_level(); let power_levels_event = OriginalStateEvent { content: RoomPowerLevelsEventContent { ban: default, events: BTreeMap::new(), events_default: int!(0), invite: int!(0), kick: default, redact: default, state_default: default, users: BTreeMap::new(), users_default: int!(0), notifications: NotificationPowerLevels::default(), }, event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!n8f893n9:example.com").to_owned(), unsigned: StateUnsigned::default(), sender: user_id!("@carl:example.com").to_owned(), state_key: EmptyStateKey, }; let actual = to_json_value(&power_levels_event).unwrap(); let expected = json!({ "content": {}, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.power_levels" }); assert_eq!(actual, expected); } #[test] fn serialization_with_all_fields() { let user = user_id!("@carl:example.com"); let power_levels_event = OriginalStateEvent { content: RoomPowerLevelsEventContent { ban: int!(23), events: btreemap! { "m.dummy".into() => int!(23) }, events_default: int!(23), invite: int!(23), kick: int!(23), redact: int!(23), state_default: int!(23), users: btreemap! { user.to_owned() => int!(23) }, users_default: int!(23), notifications: assign!(NotificationPowerLevels::new(), { room: int!(23) }), }, event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!n8f893n9:example.com").to_owned(), unsigned: StateUnsigned { age: Some(int!(100)), prev_content: Some(RoomPowerLevelsEventContent { // Make just one field different so we at least know they're two different // objects. ban: int!(42), events: btreemap! { "m.dummy".into() => int!(42) }, events_default: int!(42), invite: int!(42), kick: int!(42), redact: int!(42), state_default: int!(42), users: btreemap! { user.to_owned() => int!(42) }, users_default: int!(42), notifications: assign!(NotificationPowerLevels::new(), { room: int!(42) }), }), ..StateUnsigned::default() }, sender: user.to_owned(), state_key: EmptyStateKey, }; let actual = to_json_value(&power_levels_event).unwrap(); let expected = json!({ "content": { "ban": 23, "events": { "m.dummy": 23 }, "events_default": 23, "invite": 23, "kick": 23, "redact": 23, "state_default": 23, "users": { "@carl:example.com": 23 }, "users_default": 23, "notifications": { "room": 23 } }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.power_levels", "unsigned": { "age": 100, "prev_content": { "ban": 42, "events": { "m.dummy": 42 }, "events_default": 42, "invite": 42, "kick": 42, "redact": 42, "state_default": 42, "users": { "@carl:example.com": 42 }, "users_default": 42, "notifications": { "room": 42 }, }, } }); assert_eq!(actual, expected); } } ruma-common-0.10.5/src/events/room/redaction.rs000064400000000000000000000261231046102023000175320ustar 00000000000000//! Types for the [`m.room.redaction`] event. //! //! [`m.room.redaction`]: https://spec.matrix.org/v1.2/client-server-api/#mroomredaction use ruma_macros::{Event, EventContent}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::value::RawValue as RawJsonValue; use crate::{ events::{ EventContent, MessageLikeEventType, MessageLikeUnsigned, Redact, RedactContent, RedactedUnsigned, RedactionDeHelper, }, serde::from_raw_json_value, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; /// A possibly-redacted redaction event. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum RoomRedactionEvent { /// Original, unredacted form of the event. Original(OriginalRoomRedactionEvent), /// Redacted form of the event with minimal fields. Redacted(RedactedRoomRedactionEvent), } /// A possibly-redacted redaction event without a `room_id`. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum SyncRoomRedactionEvent { /// Original, unredacted form of the event. Original(OriginalSyncRoomRedactionEvent), /// Redacted form of the event with minimal fields. Redacted(RedactedSyncRoomRedactionEvent), } /// Redaction event. #[derive(Clone, Debug, Event)] #[allow(clippy::exhaustive_structs)] pub struct OriginalRoomRedactionEvent { /// Data specific to the event type. pub content: RoomRedactionEventContent, /// The ID of the event that was redacted. pub redacts: OwnedEventId, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, /// Additional key-value pairs not signed by the homeserver. pub unsigned: MessageLikeUnsigned, } impl Redact for OriginalRoomRedactionEvent { type Redacted = RedactedRoomRedactionEvent; fn redact( self, redaction: SyncRoomRedactionEvent, version: &crate::RoomVersionId, ) -> Self::Redacted { RedactedRoomRedactionEvent { content: self.content.redact(version), event_id: self.event_id, sender: self.sender, origin_server_ts: self.origin_server_ts, room_id: self.room_id, unsigned: RedactedUnsigned::new_because(Box::new(redaction)), } } } /// Redacted redaction event. #[derive(Clone, Debug, Event)] #[allow(clippy::exhaustive_structs)] pub struct RedactedRoomRedactionEvent { /// Data specific to the event type. pub content: RedactedRoomRedactionEventContent, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// The ID of the room associated with this event. pub room_id: OwnedRoomId, /// Additional key-value pairs not signed by the homeserver. pub unsigned: RedactedUnsigned, } /// Redaction event without a `room_id`. #[derive(Clone, Debug, Event)] #[allow(clippy::exhaustive_structs)] pub struct OriginalSyncRoomRedactionEvent { /// Data specific to the event type. pub content: RoomRedactionEventContent, /// The ID of the event that was redacted. pub redacts: OwnedEventId, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// Additional key-value pairs not signed by the homeserver. pub unsigned: MessageLikeUnsigned, } impl Redact for OriginalSyncRoomRedactionEvent { type Redacted = RedactedSyncRoomRedactionEvent; fn redact( self, redaction: SyncRoomRedactionEvent, version: &crate::RoomVersionId, ) -> Self::Redacted { RedactedSyncRoomRedactionEvent { content: self.content.redact(version), event_id: self.event_id, sender: self.sender, origin_server_ts: self.origin_server_ts, unsigned: RedactedUnsigned::new_because(Box::new(redaction)), } } } /// Redacted redaction event without a `room_id`. #[derive(Clone, Debug, Event)] #[allow(clippy::exhaustive_structs)] pub struct RedactedSyncRoomRedactionEvent { /// Data specific to the event type. pub content: RedactedRoomRedactionEventContent, /// The globally unique event identifier for the user who sent the event. pub event_id: OwnedEventId, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// Additional key-value pairs not signed by the homeserver. pub unsigned: RedactedUnsigned, } /// A redaction of an event. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.redaction", kind = MessageLike)] pub struct RoomRedactionEventContent { /// The reason for the redaction, if any. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, } impl RoomRedactionEventContent { /// Creates an empty `RoomRedactionEventContent`. pub fn new() -> Self { Self::default() } /// Creates a new `RoomRedactionEventContent` with the given reason. pub fn with_reason(reason: String) -> Self { Self { reason: Some(reason) } } } impl RoomRedactionEvent { /// Returns the `type` of this event. pub fn event_type(&self) -> MessageLikeEventType { match self { Self::Original(ev) => ev.content.event_type(), Self::Redacted(ev) => ev.content.event_type(), } } /// Returns this event's `event_id` field. pub fn event_id(&self) -> &EventId { match self { Self::Original(ev) => &ev.event_id, Self::Redacted(ev) => &ev.event_id, } } /// Returns this event's `sender` field. pub fn sender(&self) -> &UserId { match self { Self::Original(ev) => &ev.sender, Self::Redacted(ev) => &ev.sender, } } /// Returns this event's `origin_server_ts` field. pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { match self { Self::Original(ev) => ev.origin_server_ts, Self::Redacted(ev) => ev.origin_server_ts, } } /// Returns this event's `room_id` field. pub fn room_id(&self) -> &RoomId { match self { Self::Original(ev) => &ev.room_id, Self::Redacted(ev) => &ev.room_id, } } /// Get the inner `RoomRedactionEvent` if this is an unredacted event. pub fn as_original(&self) -> Option<&OriginalRoomRedactionEvent> { match self { Self::Original(v) => Some(v), _ => None, } } } impl Redact for RoomRedactionEvent { type Redacted = Self; fn redact(self, redaction: SyncRoomRedactionEvent, version: &crate::RoomVersionId) -> Self { match self { Self::Original(ev) => Self::Redacted(ev.redact(redaction, version)), Self::Redacted(ev) => Self::Redacted(ev), } } } impl<'de> Deserialize<'de> for RoomRedactionEvent { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?; if unsigned.and_then(|u| u.redacted_because).is_some() { Ok(Self::Redacted(from_raw_json_value(&json)?)) } else { Ok(Self::Original(from_raw_json_value(&json)?)) } } } impl SyncRoomRedactionEvent { /// Returns the `type` of this event. pub fn event_type(&self) -> MessageLikeEventType { match self { Self::Original(ev) => ev.content.event_type(), Self::Redacted(ev) => ev.content.event_type(), } } /// Returns this event's `event_id` field. pub fn event_id(&self) -> &EventId { match self { Self::Original(ev) => &ev.event_id, Self::Redacted(ev) => &ev.event_id, } } /// Returns this event's `sender` field. pub fn sender(&self) -> &UserId { match self { Self::Original(ev) => &ev.sender, Self::Redacted(ev) => &ev.sender, } } /// Returns this event's `origin_server_ts` field. pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { match self { Self::Original(ev) => ev.origin_server_ts, Self::Redacted(ev) => ev.origin_server_ts, } } /// Get the inner `SyncRoomRedactionEvent` if this is an unredacted event. pub fn as_original(&self) -> Option<&OriginalSyncRoomRedactionEvent> { match self { Self::Original(v) => Some(v), _ => None, } } /// Convert this sync event into a full event (one with a `room_id` field). pub fn into_full_event(self, room_id: OwnedRoomId) -> RoomRedactionEvent { match self { Self::Original(ev) => RoomRedactionEvent::Original(ev.into_full_event(room_id)), Self::Redacted(ev) => RoomRedactionEvent::Redacted(ev.into_full_event(room_id)), } } } impl From for SyncRoomRedactionEvent { fn from(full: RoomRedactionEvent) -> Self { match full { RoomRedactionEvent::Original(ev) => Self::Original(ev.into()), RoomRedactionEvent::Redacted(ev) => Self::Redacted(ev.into()), } } } impl Redact for SyncRoomRedactionEvent { type Redacted = Self; fn redact(self, redaction: SyncRoomRedactionEvent, version: &crate::RoomVersionId) -> Self { match self { Self::Original(ev) => Self::Redacted(ev.redact(redaction, version)), Self::Redacted(ev) => Self::Redacted(ev), } } } impl<'de> Deserialize<'de> for SyncRoomRedactionEvent { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; let RedactionDeHelper { unsigned } = from_raw_json_value(&json)?; if unsigned.and_then(|u| u.redacted_because).is_some() { Ok(Self::Redacted(from_raw_json_value(&json)?)) } else { Ok(Self::Original(from_raw_json_value(&json)?)) } } } ruma-common-0.10.5/src/events/room/server_acl.rs000064400000000000000000000142101046102023000177010ustar 00000000000000//! Types for the [`m.room.server_acl`] event. //! //! [`m.room.server_acl`]: https://spec.matrix.org/v1.2/client-server-api/#mroomserver_acl use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use wildmatch::WildMatch; use crate::{events::EmptyStateKey, ServerName}; /// The content of an `m.room.server_acl` event. /// /// An event to indicate which servers are permitted to participate in the room. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.server_acl", kind = State, state_key_type = EmptyStateKey)] pub struct RoomServerAclEventContent { /// Whether to allow server names that are IP address literals. /// /// This is strongly recommended to be set to false as servers running with IP literal names /// are strongly discouraged in order to require legitimate homeservers to be backed by a /// valid registered domain name. #[serde(default = "crate::serde::default_true", skip_serializing_if = "crate::serde::is_true")] pub allow_ip_literals: bool, /// The server names to allow in the room, excluding any port information. /// /// Wildcards may be used to cover a wider range of hosts, where `*` matches zero or more /// characters and `?` matches exactly one character. /// /// **Defaults to an empty list when not provided, effectively disallowing every server.** #[serde(default, skip_serializing_if = "Vec::is_empty")] pub allow: Vec, /// The server names to disallow in the room, excluding any port information. /// /// Wildcards may be used to cover a wider range of hosts, where * matches zero or more /// characters and `?` matches exactly one character. /// /// Defaults to an empty list when not provided. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub deny: Vec, } impl RoomServerAclEventContent { /// Creates a new `RoomServerAclEventContent` with the given IP literal allowance flag, allowed /// and denied servers. pub fn new(allow_ip_literals: bool, allow: Vec, deny: Vec) -> Self { Self { allow_ip_literals, allow, deny } } /// Returns true if and only if the server is allowed by the ACL rules. pub fn is_allowed(&self, server_name: &ServerName) -> bool { if !self.allow_ip_literals && server_name.is_ip_literal() { return false; } let host = server_name.host(); self.deny.iter().all(|d| !WildMatch::new(d).matches(host)) && self.allow.iter().any(|a| WildMatch::new(a).matches(host)) } } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json}; use super::RoomServerAclEventContent; use crate::{events::OriginalStateEvent, server_name}; #[test] fn default_values() { let json_data = json!({ "content": {}, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!n8f893n9:example.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.server_acl" }); let server_acl_event: OriginalStateEvent = from_json_value(json_data).unwrap(); assert!(server_acl_event.content.allow_ip_literals); assert_eq!(server_acl_event.content.allow.len(), 0); assert_eq!(server_acl_event.content.deny.len(), 0); } #[test] fn acl_ignores_port() { let acl_event = RoomServerAclEventContent { allow_ip_literals: true, allow: vec!["*".to_owned()], deny: vec!["1.1.1.1".to_owned()], }; assert!(!acl_event.is_allowed(server_name!("1.1.1.1:8000"))); } #[test] fn acl_allow_ip_literal() { let acl_event = RoomServerAclEventContent { allow_ip_literals: true, allow: vec!["*".to_owned()], deny: Vec::new(), }; assert!(acl_event.is_allowed(server_name!("1.1.1.1"))); } #[test] fn acl_deny_ip_literal() { let acl_event = RoomServerAclEventContent { allow_ip_literals: false, allow: vec!["*".to_owned()], deny: Vec::new(), }; assert!(!acl_event.is_allowed(server_name!("1.1.1.1"))); } #[test] fn acl_deny() { let acl_event = RoomServerAclEventContent { allow_ip_literals: false, allow: vec!["*".to_owned()], deny: vec!["matrix.org".to_owned()], }; assert!(!acl_event.is_allowed(server_name!("matrix.org"))); assert!(acl_event.is_allowed(server_name!("conduit.rs"))); } #[test] fn acl_explicit_allow() { let acl_event = RoomServerAclEventContent { allow_ip_literals: false, allow: vec!["conduit.rs".to_owned()], deny: Vec::new(), }; assert!(!acl_event.is_allowed(server_name!("matrix.org"))); assert!(acl_event.is_allowed(server_name!("conduit.rs"))); } #[test] fn acl_explicit_glob_1() { let acl_event = RoomServerAclEventContent { allow_ip_literals: false, allow: vec!["*.matrix.org".to_owned()], deny: Vec::new(), }; assert!(!acl_event.is_allowed(server_name!("matrix.org"))); assert!(acl_event.is_allowed(server_name!("server.matrix.org"))); } #[test] fn acl_explicit_glob_2() { let acl_event = RoomServerAclEventContent { allow_ip_literals: false, allow: vec!["matrix??.org".to_owned()], deny: Vec::new(), }; assert!(!acl_event.is_allowed(server_name!("matrix1.org"))); assert!(acl_event.is_allowed(server_name!("matrix02.org"))); } #[test] fn acl_ipv6_glob() { let acl_event = RoomServerAclEventContent { allow_ip_literals: true, allow: vec!["[2001:db8:1234::1]".to_owned()], deny: Vec::new(), }; assert!(!acl_event.is_allowed(server_name!("[2001:db8:1234::2]"))); assert!(acl_event.is_allowed(server_name!("[2001:db8:1234::1]"))); } } ruma-common-0.10.5/src/events/room/third_party_invite.rs000064400000000000000000000063471046102023000214770ustar 00000000000000//! Types for the [`m.room.third_party_invite`] event. //! //! [`m.room.third_party_invite`]: https://spec.matrix.org/v1.2/client-server-api/#mroomthird_party_invite use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::serde::Base64; /// The content of an `m.room.third_party_invite` event. /// /// An invitation to a room issued to a third party identifier, rather than a matrix user ID. /// /// Acts as an `m.room.member` invite event, where there isn't a target user_id to invite. This /// event contains a token and a public key whose private key must be used to sign the token. /// Any user who can present that signature may use this invitation to join the target room. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.third_party_invite", kind = State, state_key_type = String)] pub struct RoomThirdPartyInviteEventContent { /// A user-readable string which represents the user who has been invited. /// /// If you activate the `compat` feature, this field being absent in JSON will result in an /// empty string here during deserialization. #[cfg_attr(feature = "compat", serde(default))] pub display_name: String, /// A URL which can be fetched to validate whether the key has been revoked. /// /// If you activate the `compat` feature, this field being absent in JSON will result in an /// empty string here during deserialization. #[cfg_attr(feature = "compat", serde(default))] pub key_validity_url: String, /// A base64-encoded Ed25519 key with which the token must be signed. /// /// If you activate the `compat` feature, this field being absent in JSON will result in an /// empty string here during deserialization. #[cfg_attr(feature = "compat", serde(default = "Base64::empty"))] pub public_key: Base64, /// Keys with which the token may be signed. #[serde(skip_serializing_if = "Option::is_none")] pub public_keys: Option>, } impl RoomThirdPartyInviteEventContent { /// Creates a new `RoomThirdPartyInviteEventContent` with the given display name, key validity /// url and public key. pub fn new(display_name: String, key_validity_url: String, public_key: Base64) -> Self { Self { display_name, key_validity_url, public_key, public_keys: None } } } /// A public key for signing a third party invite token. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PublicKey { /// An optional URL which can be fetched to validate whether the key has been revoked. /// /// The URL must return a JSON object containing a boolean property named 'valid'. /// If this URL is absent, the key must be considered valid indefinitely. #[serde(skip_serializing_if = "Option::is_none")] pub key_validity_url: Option, /// A base64-encoded Ed25519 key with which the token must be signed. pub public_key: Base64, } impl PublicKey { /// Creates a new `PublicKey` with the given base64-encoded ed25519 key. pub fn new(public_key: Base64) -> Self { Self { key_validity_url: None, public_key } } } ruma-common-0.10.5/src/events/room/thumbnail_source_serde.rs000064400000000000000000000146201046102023000223060ustar 00000000000000//! De-/serialization functions for `Option` objects representing a thumbnail source. use serde::{ ser::{SerializeStruct, Serializer}, Deserialize, Deserializer, }; use crate::OwnedMxcUri; use super::{EncryptedFile, MediaSource}; /// Serializes a MediaSource to a thumbnail source. pub fn serialize(source: &Option, serializer: S) -> Result where S: Serializer, { if let Some(source) = source { let mut st = serializer.serialize_struct("ThumbnailSource", 1)?; match source { MediaSource::Plain(url) => st.serialize_field("thumbnail_url", url)?, MediaSource::Encrypted(file) => st.serialize_field("thumbnail_file", file)?, } st.end() } else { serializer.serialize_none() } } /// Deserializes a thumbnail source to a MediaSource. pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] pub struct ThumbnailSourceJsonRepr { thumbnail_url: Option, thumbnail_file: Option>, } match ThumbnailSourceJsonRepr::deserialize(deserializer)? { ThumbnailSourceJsonRepr { thumbnail_url: None, thumbnail_file: None } => Ok(None), // Prefer file if it is set ThumbnailSourceJsonRepr { thumbnail_file: Some(file), .. } => { Ok(Some(MediaSource::Encrypted(file))) } ThumbnailSourceJsonRepr { thumbnail_url: Some(url), .. } => { Ok(Some(MediaSource::Plain(url))) } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{ events::room::{EncryptedFileInit, JsonWebKeyInit, MediaSource}, mxc_uri, serde::Base64, }; #[derive(Clone, Debug, Deserialize, Serialize)] struct ThumbnailSourceTest { #[serde(flatten, with = "super", skip_serializing_if = "Option::is_none")] source: Option, } #[test] fn deserialize_plain() { let json = json!({ "thumbnail_url": "mxc://notareal.hs/abcdef" }); let url = assert_matches!( serde_json::from_value::(json), Ok(ThumbnailSourceTest { source: Some(MediaSource::Plain(url)) }) => url ); assert_eq!(url, "mxc://notareal.hs/abcdef"); } #[test] fn deserialize_encrypted() { let json = json!({ "thumbnail_file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, }); let file = assert_matches!( serde_json::from_value::(json), Ok(ThumbnailSourceTest { source: Some(MediaSource::Encrypted(file)) }) => file ); assert_eq!(file.url, "mxc://notareal.hs/abcdef"); } #[test] fn deserialize_none_by_absence() { let json = json!({}); assert_matches!( serde_json::from_value::(json).unwrap(), ThumbnailSourceTest { source: None } ); } #[test] fn deserialize_none_by_null_plain() { let json = json!({ "thumbnail_url": null }); assert_matches!( serde_json::from_value::(json).unwrap(), ThumbnailSourceTest { source: None } ); } #[test] fn deserialize_none_by_null_encrypted() { let json = json!({ "thumbnail_file": null }); assert_matches!( serde_json::from_value::(json).unwrap(), ThumbnailSourceTest { source: None } ); } #[test] fn serialize_plain() { let request = ThumbnailSourceTest { source: Some(MediaSource::Plain(mxc_uri!("mxc://notareal.hs/abcdef").into())), }; assert_eq!( serde_json::to_value(&request).unwrap(), json!({ "thumbnail_url": "mxc://notareal.hs/abcdef" }) ); } #[test] fn serialize_encrypted() { let request = ThumbnailSourceTest { source: Some(MediaSource::Encrypted(Box::new( EncryptedFileInit { url: mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), ))), }; assert_eq!( serde_json::to_value(&request).unwrap(), json!({ "thumbnail_file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, }) ); } #[test] fn serialize_none() { let request = ThumbnailSourceTest { source: None }; assert_eq!(serde_json::to_value(&request).unwrap(), json!({})); } } ruma-common-0.10.5/src/events/room/tombstone.rs000064400000000000000000000024241046102023000175720ustar 00000000000000//! Types for the [`m.room.tombstone`] event. //! //! [`m.room.tombstone`]: https://spec.matrix.org/v1.2/client-server-api/#mroomtombstone use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{events::EmptyStateKey, OwnedRoomId}; /// The content of an `m.room.tombstone` event. /// /// A state event signifying that a room has been upgraded to a different room version, and that /// clients should go there. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[ruma_event(type = "m.room.tombstone", kind = State, state_key_type = EmptyStateKey)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RoomTombstoneEventContent { /// A server-defined message. /// /// If you activate the `compat` feature, this field being absent in JSON will result in an /// empty string here during deserialization. #[cfg_attr(feature = "compat", serde(default))] pub body: String, /// The new room the client should be visiting. pub replacement_room: OwnedRoomId, } impl RoomTombstoneEventContent { /// Creates a new `RoomTombstoneEventContent` with the given body and replacement room ID. pub fn new(body: String, replacement_room: OwnedRoomId) -> Self { Self { body, replacement_room } } } ruma-common-0.10.5/src/events/room/topic.rs000064400000000000000000000015061046102023000166760ustar 00000000000000//! Types for the [`m.room.topic`] event. //! //! [`m.room.topic`]: https://spec.matrix.org/v1.2/client-server-api/#mroomtopic use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::events::EmptyStateKey; /// The content of an `m.room.topic` event. /// /// A topic is a short message detailing what is currently being discussed in the room. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.topic", kind = State, state_key_type = EmptyStateKey)] pub struct RoomTopicEventContent { /// The topic text. pub topic: String, } impl RoomTopicEventContent { /// Creates a new `RoomTopicEventContent` with the given topic. pub fn new(topic: String) -> Self { Self { topic } } } ruma-common-0.10.5/src/events/room.rs000064400000000000000000000343121046102023000155610ustar 00000000000000//! Modules for events in the `m.room` namespace. //! //! This module also contains types shared by events in its child namespaces. // https://github.com/rust-lang/rust-clippy/issues/9111 #![allow(clippy::needless_borrow)] use std::collections::BTreeMap; use js_int::UInt; use serde::{de, Deserialize, Serialize}; #[cfg(feature = "unstable-msc3551")] use super::file::{EncryptedContent, EncryptedContentInit, FileContent}; #[cfg(feature = "unstable-msc3552")] use super::{ file::FileContentInfo, image::{ImageContent, ThumbnailContent, ThumbnailFileContent, ThumbnailFileContentInfo}, }; #[cfg(feature = "unstable-msc3551")] use crate::MxcUri; use crate::{ serde::{base64::UrlSafe, Base64}, OwnedMxcUri, }; pub mod aliases; pub mod avatar; pub mod canonical_alias; pub mod create; pub mod encrypted; pub mod encryption; pub mod guest_access; pub mod history_visibility; pub mod join_rules; pub mod member; pub mod message; pub mod name; pub mod pinned_events; pub mod power_levels; pub mod redaction; pub mod server_acl; pub mod third_party_invite; mod thumbnail_source_serde; pub mod tombstone; pub mod topic; /// The source of a media file. #[derive(Clone, Debug, Serialize)] #[allow(clippy::exhaustive_enums)] pub enum MediaSource { /// The MXC URI to the unencrypted media file. #[serde(rename = "url")] Plain(OwnedMxcUri), /// The encryption info of the encrypted media file. #[serde(rename = "file")] Encrypted(Box), } #[cfg(feature = "unstable-msc3551")] impl MediaSource { pub(crate) fn into_extensible_content(self) -> (OwnedMxcUri, Option) { match self { MediaSource::Plain(url) => (url, None), MediaSource::Encrypted(encrypted_file) => { let EncryptedFile { url, key, iv, hashes, v } = *encrypted_file; (url, Some(EncryptedContentInit { key, iv, hashes, v }.into())) } } } } // Custom implementation of `Deserialize`, because serde doesn't guarantee what variant will be // deserialized for "externally tagged"¹ enums where multiple "tag" fields exist. // // ¹ https://serde.rs/enum-representations.html impl<'de> Deserialize<'de> for MediaSource { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Deserialize)] pub struct MediaSourceJsonRepr { url: Option, file: Option>, } match MediaSourceJsonRepr::deserialize(deserializer)? { MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")), // Prefer file if it is set MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)), MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)), } } } #[cfg(feature = "unstable-msc3551")] impl From<&FileContent> for MediaSource { fn from(content: &FileContent) -> Self { let FileContent { url, encryption_info, .. } = content; if let Some(encryption_info) = encryption_info.as_deref() { Self::Encrypted(Box::new(EncryptedFile::from_extensible_content(url, encryption_info))) } else { Self::Plain(url.to_owned()) } } } #[cfg(feature = "unstable-msc3552")] impl From<&ThumbnailFileContent> for MediaSource { fn from(content: &ThumbnailFileContent) -> Self { let ThumbnailFileContent { url, encryption_info, .. } = content; if let Some(encryption_info) = encryption_info.as_deref() { Self::Encrypted(Box::new(EncryptedFile::from_extensible_content(url, encryption_info))) } else { Self::Plain(url.to_owned()) } } } /// Metadata about an image. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ImageInfo { /// The height of the image in pixels. #[serde(rename = "h", skip_serializing_if = "Option::is_none")] pub height: Option, /// The width of the image in pixels. #[serde(rename = "w", skip_serializing_if = "Option::is_none")] pub width: Option, /// The MIME type of the image, e.g. "image/png." #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The file size of the image in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, /// Metadata about the image referred to in `thumbnail_source`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option>, /// The source of the thumbnail of the image. #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")] pub thumbnail_source: Option, /// The [BlurHash](https://blurha.sh) for this image. /// /// This uses the unstable prefix in /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). #[cfg(feature = "unstable-msc2448")] #[serde( rename = "xyz.amorgan.blurhash", alias = "blurhash", skip_serializing_if = "Option::is_none" )] pub blurhash: Option, } impl ImageInfo { /// Creates an empty `ImageInfo`. pub fn new() -> Self { Self::default() } /// Create an `ImageInfo` from the given file info, image info and thumbnail. /// /// Returns `None` if the `ImageInfo` would be empty. #[cfg(feature = "unstable-msc3552")] fn from_extensible_content( file_info: Option<&FileContentInfo>, image: &ImageContent, thumbnail: &[ThumbnailContent], ) -> Option { if file_info.is_none() && image.is_empty() && thumbnail.is_empty() { None } else { let (mimetype, size) = file_info .map(|info| (info.mimetype.to_owned(), info.size.to_owned())) .unwrap_or_default(); let ImageContent { height, width } = image.to_owned(); let (thumbnail_source, thumbnail_info) = thumbnail .get(0) .map(|thumbnail| { let source = (&thumbnail.file).into(); let info = ThumbnailInfo::from_extensible_content( thumbnail.file.info.as_deref(), thumbnail.image.as_deref(), ) .map(Box::new); (Some(source), info) }) .unwrap_or_default(); Some(Self { height, width, mimetype, size, thumbnail_source, thumbnail_info, #[cfg(feature = "unstable-msc2448")] blurhash: None, }) } } } /// Metadata about a thumbnail. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ThumbnailInfo { /// The height of the thumbnail in pixels. #[serde(rename = "h", skip_serializing_if = "Option::is_none")] pub height: Option, /// The width of the thumbnail in pixels. #[serde(rename = "w", skip_serializing_if = "Option::is_none")] pub width: Option, /// The MIME type of the thumbnail, e.g. "image/png." #[serde(skip_serializing_if = "Option::is_none")] pub mimetype: Option, /// The file size of the thumbnail in bytes. #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, } impl ThumbnailInfo { /// Creates an empty `ThumbnailInfo`. pub fn new() -> Self { Self::default() } /// Create a `ThumbnailInfo` with the given file info and image info. /// /// Returns `None` if the `ThumbnailInfo` would be empty. #[cfg(feature = "unstable-msc3552")] fn from_extensible_content( file_info: Option<&ThumbnailFileContentInfo>, image: Option<&ImageContent>, ) -> Option { if file_info.is_none() && image.is_none() { None } else { let ThumbnailFileContentInfo { mimetype, size } = file_info.map(ToOwned::to_owned).unwrap_or_default(); let ImageContent { height, width } = image.map(ToOwned::to_owned).unwrap_or_default(); Some(Self { height, width, mimetype, size }) } } } /// A file sent to a room with end-to-end encryption enabled. /// /// To create an instance of this type, first create a `EncryptedFileInit` and convert it via /// `EncryptedFile::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct EncryptedFile { /// The URL to the file. pub url: OwnedMxcUri, /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. pub key: JsonWebKey, /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. pub iv: Base64, /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. /// /// Clients should support the SHA-256 hash, which uses the key sha256. pub hashes: BTreeMap, /// Version of the encrypted attachments protocol. /// /// Must be `v2`. pub v: String, } #[cfg(feature = "unstable-msc3551")] impl EncryptedFile { /// Create an `EncryptedFile` from the given url and encryption info. fn from_extensible_content(url: &MxcUri, encryption_info: &EncryptedContent) -> Self { let EncryptedContent { key, iv, hashes, v } = encryption_info.to_owned(); Self { url: url.to_owned(), key, iv, hashes, v } } } /// Initial set of fields of `EncryptedFile`. /// /// This struct will not be updated even if additional fields are added to `EncryptedFile` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct EncryptedFileInit { /// The URL to the file. pub url: OwnedMxcUri, /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. pub key: JsonWebKey, /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. pub iv: Base64, /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. /// /// Clients should support the SHA-256 hash, which uses the key sha256. pub hashes: BTreeMap, /// Version of the encrypted attachments protocol. /// /// Must be `v2`. pub v: String, } impl From for EncryptedFile { fn from(init: EncryptedFileInit) -> Self { let EncryptedFileInit { url, key, iv, hashes, v } = init; Self { url, key, iv, hashes, v } } } /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. /// /// To create an instance of this type, first create a `JsonWebKeyInit` and convert it via /// `JsonWebKey::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct JsonWebKey { /// Key type. /// /// Must be `oct`. pub kty: String, /// Key operations. /// /// Must at least contain `encrypt` and `decrypt`. pub key_ops: Vec, /// Algorithm. /// /// Must be `A256CTR`. pub alg: String, /// The key, encoded as url-safe unpadded base64. pub k: Base64, /// Extractable. /// /// Must be `true`. This is a /// [W3C extension](https://w3c.github.io/webcrypto/#iana-section-jwk). pub ext: bool, } /// Initial set of fields of `JsonWebKey`. /// /// This struct will not be updated even if additional fields are added to `JsonWebKey` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct JsonWebKeyInit { /// Key type. /// /// Must be `oct`. pub kty: String, /// Key operations. /// /// Must at least contain `encrypt` and `decrypt`. pub key_ops: Vec, /// Algorithm. /// /// Must be `A256CTR`. pub alg: String, /// The key, encoded as url-safe unpadded base64. pub k: Base64, /// Extractable. /// /// Must be `true`. This is a /// [W3C extension](https://w3c.github.io/webcrypto/#iana-section-jwk). pub ext: bool, } impl From for JsonWebKey { fn from(init: JsonWebKeyInit) -> Self { let JsonWebKeyInit { kty, key_ops, alg, k, ext } = init; Self { kty, key_ops, alg, k, ext } } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use serde::Deserialize; use serde_json::{from_value as from_json_value, json}; use crate::{mxc_uri, serde::Base64}; use super::{EncryptedFile, JsonWebKey, MediaSource}; #[derive(Deserialize)] struct MsgWithAttachment { #[allow(dead_code)] body: String, #[serde(flatten)] source: MediaSource, } fn dummy_jwt() -> JsonWebKey { JsonWebKey { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::new(vec![0; 64]), ext: true, } } fn encrypted_file() -> EncryptedFile { EncryptedFile { url: mxc_uri!("mxc://localhost/encryptedfile").to_owned(), key: dummy_jwt(), iv: Base64::new(vec![0; 64]), hashes: BTreeMap::new(), v: "v2".to_owned(), } } #[test] fn prefer_encrypted_attachment_over_plain() { let msg: MsgWithAttachment = from_json_value(json!({ "body": "", "url": "mxc://localhost/file", "file": encrypted_file(), })) .unwrap(); assert_matches!(msg.source, MediaSource::Encrypted(_)); // As above, but with the file field before the url field let msg: MsgWithAttachment = from_json_value(json!({ "body": "", "file": encrypted_file(), "url": "mxc://localhost/file", })) .unwrap(); assert_matches!(msg.source, MediaSource::Encrypted(_)); } } ruma-common-0.10.5/src/events/room_key.rs000064400000000000000000000047111046102023000164310ustar 00000000000000//! Types for the [`m.room_key`] event. //! //! [`m.room_key`]: https://spec.matrix.org/v1.2/client-server-api/#mroom_key use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{EventEncryptionAlgorithm, OwnedRoomId}; /// The content of an `m.room_key` event. /// /// Typically encrypted as an `m.room.encrypted` event, then sent as a to-device event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room_key", kind = ToDevice)] pub struct ToDeviceRoomKeyEventContent { /// The encryption algorithm the key in this event is to be used with. /// /// Must be `m.megolm.v1.aes-sha2`. pub algorithm: EventEncryptionAlgorithm, /// The room where the key is used. pub room_id: OwnedRoomId, /// The ID of the session that the key is for. pub session_id: String, /// The key to be exchanged. pub session_key: String, } impl ToDeviceRoomKeyEventContent { /// Creates a new `ToDeviceRoomKeyEventContent` with the given algorithm, room ID, session ID /// and session key. pub fn new( algorithm: EventEncryptionAlgorithm, room_id: OwnedRoomId, session_id: String, session_key: String, ) -> Self { Self { algorithm, room_id, session_id, session_key } } } #[cfg(test)] mod tests { use crate::{room_id, user_id, EventEncryptionAlgorithm}; use serde_json::{json, to_value as to_json_value}; use super::ToDeviceRoomKeyEventContent; use crate::events::ToDeviceEvent; #[test] fn serialization() { let ev = ToDeviceEvent { content: ToDeviceRoomKeyEventContent { algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, room_id: room_id!("!testroomid:example.org").to_owned(), session_id: "SessId".into(), session_key: "SessKey".into(), }, sender: user_id!("@user:example.org").to_owned(), }; assert_eq!( to_json_value(ev).unwrap(), json!({ "type": "m.room_key", "content": { "algorithm": "m.megolm.v1.aes-sha2", "room_id": "!testroomid:example.org", "session_id": "SessId", "session_key": "SessKey", }, "sender": "@user:example.org", }) ); } } ruma-common-0.10.5/src/events/room_key_request.rs000064400000000000000000000061531046102023000202030ustar 00000000000000//! Types for the [`m.room_key_request`] event. //! //! [`m.room_key_request`]: https://spec.matrix.org/v1.2/client-server-api/#mroom_key_request use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{ serde::StringEnum, EventEncryptionAlgorithm, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, PrivOwnedStr, }; /// The content of an `m.room_key_request` event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room_key_request", kind = ToDevice)] pub struct ToDeviceRoomKeyRequestEventContent { /// Whether this is a new key request or a cancellation of a previous request. pub action: Action, /// Information about the requested key. /// /// Required if action is `request`. pub body: Option, /// ID of the device requesting the key. pub requesting_device_id: OwnedDeviceId, /// A random string uniquely identifying the request for a key. /// /// If the key is requested multiple times, it should be reused. It should also reused /// in order to cancel a request. pub request_id: OwnedTransactionId, } impl ToDeviceRoomKeyRequestEventContent { /// Creates a new `ToDeviceRoomKeyRequestEventContent` with the given action, boyd, device ID /// and request ID. pub fn new( action: Action, body: Option, requesting_device_id: OwnedDeviceId, request_id: OwnedTransactionId, ) -> Self { Self { action, body, requesting_device_id, request_id } } } /// A new key request or a cancellation of a previous request. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum Action { /// Request a key. Request, /// Cancel a request for a key. #[ruma_enum(rename = "request_cancellation")] CancelRequest, #[doc(hidden)] _Custom(PrivOwnedStr), } /// Information about a requested key. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RequestedKeyInfo { /// The encryption algorithm the requested key in this event is to be used with. pub algorithm: EventEncryptionAlgorithm, /// The room where the key is used. pub room_id: OwnedRoomId, /// The Curve25519 key of the device which initiated the session originally. #[deprecated = "this field still needs to be sent but should not be used when received"] pub sender_key: String, /// The ID of the session that the key is for. pub session_id: String, } impl RequestedKeyInfo { /// Creates a new `RequestedKeyInfo` with the given algorithm, room ID, sender key and session /// ID. pub fn new( algorithm: EventEncryptionAlgorithm, room_id: OwnedRoomId, sender_key: String, session_id: String, ) -> Self { #[allow(deprecated)] Self { algorithm, room_id, sender_key, session_id } } } ruma-common-0.10.5/src/events/secret/request.rs000064400000000000000000000223401046102023000175600ustar 00000000000000//! Types for the [`m.secret.request`] event. //! //! [`m.secret.request`]: https://spec.matrix.org/v1.2/client-server-api/#msecretrequest use ruma_macros::EventContent; use serde::{ser::SerializeStruct, Deserialize, Serialize}; use crate::{serde::StringEnum, OwnedDeviceId, OwnedTransactionId, PrivOwnedStr}; /// The content of an `m.secret.request` event. /// /// Event sent by a client to request a secret from another device or to cancel a previous request. /// /// It is sent as an unencrypted to-device event. #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.secret.request", kind = ToDevice)] pub struct ToDeviceSecretRequestEventContent { /// The action for the request. #[serde(flatten)] pub action: RequestAction, /// The ID of the device requesting the event. pub requesting_device_id: OwnedDeviceId, /// A random string uniquely identifying (with respect to the requester and the target) the /// target for a secret. /// /// If the secret is requested from multiple devices at the same time, the same ID may be used /// for every target. The same ID is also used in order to cancel a previous request. pub request_id: OwnedTransactionId, } impl ToDeviceSecretRequestEventContent { /// Creates a new `ToDeviceRequestEventContent` with the given action, requesting device ID and /// request ID. pub fn new( action: RequestAction, requesting_device_id: OwnedDeviceId, request_id: OwnedTransactionId, ) -> Self { Self { action, requesting_device_id, request_id } } } /// Action for an `m.secret.request` event. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(try_from = "RequestActionJsonRepr")] pub enum RequestAction { /// Request a secret by its name. Request(SecretName), /// Cancel a request for a secret. RequestCancellation, #[doc(hidden)] _Custom(PrivOwnedStr), } impl Serialize for RequestAction { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut st = serializer.serialize_struct("request_action", 2)?; match self { Self::Request(name) => { st.serialize_field("name", name)?; st.serialize_field("action", "request")?; st.end() } Self::RequestCancellation => { st.serialize_field("action", "request_cancellation")?; st.end() } RequestAction::_Custom(custom) => { st.serialize_field("action", &custom.0)?; st.end() } } } } #[derive(Deserialize)] struct RequestActionJsonRepr { action: String, name: Option, } impl TryFrom for RequestAction { type Error = &'static str; fn try_from(value: RequestActionJsonRepr) -> Result { match value.action.as_str() { "request" => { if let Some(name) = value.name { Ok(RequestAction::Request(name)) } else { Err("A secret name is required when the action is \"request\".") } } "request_cancellation" => Ok(RequestAction::RequestCancellation), _ => Ok(RequestAction::_Custom(PrivOwnedStr(value.action.into()))), } } } /// The name of a secret. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum SecretName { /// Cross-signing master key (m.cross_signing.master). #[ruma_enum(rename = "m.cross_signing.master")] CrossSigningMasterKey, /// Cross-signing user-signing key (m.cross_signing.user_signing). #[ruma_enum(rename = "m.cross_signing.user_signing")] CrossSigningUserSigningKey, /// Cross-signing self-signing key (m.cross_signing.self_signing). #[ruma_enum(rename = "m.cross_signing.self_signing")] CrossSigningSelfSigningKey, /// Recovery key (m.megolm_backup.v1). #[ruma_enum(rename = "m.megolm_backup.v1")] RecoveryKey, #[doc(hidden)] _Custom(PrivOwnedStr), } #[cfg(test)] mod test { use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{RequestAction, SecretName, ToDeviceSecretRequestEventContent}; use crate::PrivOwnedStr; #[test] fn secret_request_serialization() { let content = ToDeviceSecretRequestEventContent::new( RequestAction::Request("org.example.some.secret".into()), "ABCDEFG".into(), "randomly_generated_id_9573".into(), ); let json = json!({ "name": "org.example.some.secret", "action": "request", "requesting_device_id": "ABCDEFG", "request_id": "randomly_generated_id_9573" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn secret_request_recovery_key_serialization() { let content = ToDeviceSecretRequestEventContent::new( RequestAction::Request(SecretName::RecoveryKey), "XYZxyz".into(), "this_is_a_request_id".into(), ); let json = json!({ "name": "m.megolm_backup.v1", "action": "request", "requesting_device_id": "XYZxyz", "request_id": "this_is_a_request_id" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn secret_custom_action_serialization() { let content = ToDeviceSecretRequestEventContent::new( RequestAction::_Custom(PrivOwnedStr("my_custom_action".into())), "XYZxyz".into(), "this_is_a_request_id".into(), ); let json = json!({ "action": "my_custom_action", "requesting_device_id": "XYZxyz", "request_id": "this_is_a_request_id" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn secret_request_cancellation_serialization() { let content = ToDeviceSecretRequestEventContent::new( RequestAction::RequestCancellation, "ABCDEFG".into(), "randomly_generated_id_9573".into(), ); let json = json!({ "action": "request_cancellation", "requesting_device_id": "ABCDEFG", "request_id": "randomly_generated_id_9573" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn secret_request_deserialization() { let json = json!({ "name": "org.example.some.secret", "action": "request", "requesting_device_id": "ABCDEFG", "request_id": "randomly_generated_id_9573" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.requesting_device_id, "ABCDEFG"); assert_eq!(content.request_id, "randomly_generated_id_9573"); let secret = assert_matches!( content.action, RequestAction::Request(secret) => secret ); assert_eq!(secret.as_str(), "org.example.some.secret"); } #[test] fn secret_request_cancellation_deserialization() { let json = json!({ "action": "request_cancellation", "requesting_device_id": "ABCDEFG", "request_id": "randomly_generated_id_9573" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.requesting_device_id, "ABCDEFG"); assert_eq!(content.request_id, "randomly_generated_id_9573"); assert_matches!(content.action, RequestAction::RequestCancellation); } #[test] fn secret_request_recovery_key_deserialization() { let json = json!({ "name": "m.megolm_backup.v1", "action": "request", "requesting_device_id": "XYZxyz", "request_id": "this_is_a_request_id" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.requesting_device_id, "XYZxyz"); assert_eq!(content.request_id, "this_is_a_request_id"); let secret = assert_matches!( content.action, RequestAction::Request(secret) => secret ); assert_eq!(secret, SecretName::RecoveryKey); } #[test] fn secret_custom_action_deserialization() { let json = json!({ "action": "my_custom_action", "requesting_device_id": "XYZxyz", "request_id": "this_is_a_request_id" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.requesting_device_id, "XYZxyz"); assert_eq!(content.request_id, "this_is_a_request_id"); assert_eq!(content.action, RequestAction::_Custom(PrivOwnedStr("my_custom_action".into()))); } } ruma-common-0.10.5/src/events/secret/send.rs000064400000000000000000000021461046102023000170230ustar 00000000000000//! Types for the [`m.secret.send`] event. //! //! [`m.secret.send`]: https://spec.matrix.org/v1.2/client-server-api/#msecretsend use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::OwnedTransactionId; /// The content of an `m.secret.send` event. /// /// An event sent by a client to share a secret with another device, in response to an /// `m.secret.request` event. /// /// It must be encrypted as an `m.room.encrypted` event, then sent as a to-device event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.secret.send", kind = ToDevice)] pub struct ToDeviceSecretSendEventContent { /// The ID of the request that this is a response to. pub request_id: OwnedTransactionId, /// The contents of the secret. pub secret: String, } impl ToDeviceSecretSendEventContent { /// Creates a new `SecretSendEventContent` with the given request ID and secret. pub fn new(request_id: OwnedTransactionId, secret: String) -> Self { Self { request_id, secret } } } ruma-common-0.10.5/src/events/secret.rs000064400000000000000000000001231046102023000160630ustar 00000000000000//! Module for events in the `m.secret` namespace. pub mod request; pub mod send; ruma-common-0.10.5/src/events/secret_storage/default_key.rs000064400000000000000000000011351046102023000221070ustar 00000000000000//! Types for the [`m.secret_storage.default_key`] event. //! //! [`m.secret_storage.default_key`]: https://spec.matrix.org/v1.2/client-server-api/#key-storage use ruma_common::events::macros::EventContent; use serde::{Deserialize, Serialize}; /// The payload for `DefaultKeyEvent`. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.secret_storage.default_key", kind = GlobalAccountData)] pub struct SecretStorageDefaultKeyEventContent { /// The ID of the default key. pub key: String, } ruma-common-0.10.5/src/events/secret_storage/key.rs000064400000000000000000000220611046102023000204040ustar 00000000000000//! Types for the [`m.secret_storage.key.*`] event. //! //! [`m.secret_storage.key.*`]: https://spec.matrix.org/v1.2/client-server-api/#key-storage use js_int::{uint, UInt}; use serde::{Deserialize, Serialize}; use crate::{events::macros::EventContent, identifiers::KeyDerivationAlgorithm, serde::Base64}; /// A passphrase from which a key is to be derived. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PassPhrase { /// The algorithm to use to generate the key from the passphrase. /// /// Must be `m.pbkdf2`. pub algorithm: KeyDerivationAlgorithm, /// The salt used in PBKDF2. pub salt: String, /// The number of iterations to use in PBKDF2. pub iterations: UInt, /// The number of bits to generate for the key. /// /// Defaults to 256 #[serde(default = "default_bits", skip_serializing_if = "is_default_bits")] pub bits: UInt, } impl PassPhrase { /// Creates a new `PassPhrase` with a given salt and number of iterations. pub fn new(salt: String, iterations: UInt) -> Self { Self { algorithm: KeyDerivationAlgorithm::Pbkfd2, salt, iterations, bits: default_bits() } } } fn default_bits() -> UInt { uint!(256) } fn is_default_bits(val: &UInt) -> bool { *val == default_bits() } /// A key description encrypted using a specified algorithm. #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)] pub struct SecretStorageKeyEventContent { /// The ID of the key. #[ruma_event(type_fragment)] #[serde(skip)] pub key_id: String, /// The name of the key. pub name: String, /// The encryption algorithm used for this key. /// /// Currently, only `m.secret_storage.v1.aes-hmac-sha2` is supported. #[serde(flatten)] pub algorithm: SecretEncryptionAlgorithm, /// The passphrase from which to generate the key. #[serde(skip_serializing_if = "Option::is_none")] pub passphrase: Option, } impl SecretStorageKeyEventContent { /// Creates a `KeyDescription` with the given name. pub fn new(key_id: String, name: String, algorithm: SecretEncryptionAlgorithm) -> Self { Self { key_id, name, algorithm, passphrase: None } } } /// An algorithm and its properties, used to encrypt a secret. #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "algorithm")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum SecretEncryptionAlgorithm { #[serde(rename = "m.secret_storage.v1.aes-hmac-sha2")] /// Encrypted using the `m.secrect_storage.v1.aes-hmac-sha2` algorithm. /// /// Secrets using this method are encrypted using AES-CTR-256 and authenticated using /// HMAC-SHA-256. SecretStorageV1AesHmacSha2 { /// The 16-byte initialization vector, encoded as base64. iv: Base64, /// The MAC, encoded as base64. mac: Base64, }, } #[cfg(test)] mod tests { use assert_matches::assert_matches; use js_int::uint; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{PassPhrase, SecretEncryptionAlgorithm, SecretStorageKeyEventContent}; use crate::{events::GlobalAccountDataEvent, serde::Base64, KeyDerivationAlgorithm}; #[test] fn test_key_description_serialization() { let content = SecretStorageKeyEventContent::new( "my_key".into(), "my_key".into(), SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), }, ); let json = json!({ "name": "my_key", "algorithm": "m.secret_storage.v1.aes-hmac-sha2", "iv": "YWJjZGVmZ2hpamtsbW5vcA", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn test_key_description_deserialization() { let json = json!({ "name": "my_key", "algorithm": "m.secret_storage.v1.aes-hmac-sha2", "iv": "YWJjZGVmZ2hpamtsbW5vcA", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" }); let content = from_json_value::(json).unwrap(); assert_eq!(content.name, "my_key"); assert_matches!(content.passphrase, None); let (iv, mac) = assert_matches!( content.algorithm, SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { iv, mac, } => (iv, mac) ); assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA"); assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"); } #[test] fn test_key_description_with_passphrase_serialization() { let content = SecretStorageKeyEventContent { passphrase: Some(PassPhrase::new("rocksalt".into(), uint!(8))), ..SecretStorageKeyEventContent::new( "my_key".into(), "my_key".into(), SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), }, ) }; let json = json!({ "name": "my_key", "algorithm": "m.secret_storage.v1.aes-hmac-sha2", "iv": "YWJjZGVmZ2hpamtsbW5vcA", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U", "passphrase": { "algorithm": "m.pbkdf2", "salt": "rocksalt", "iterations": 8 } }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn test_key_description_with_passphrase_deserialization() { let json = json!({ "name": "my_key", "algorithm": "m.secret_storage.v1.aes-hmac-sha2", "iv": "YWJjZGVmZ2hpamtsbW5vcA", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U", "passphrase": { "algorithm": "m.pbkdf2", "salt": "rocksalt", "iterations": 8, "bits": 256 } }); let content = from_json_value::(json).unwrap(); assert_eq!(content.name, "my_key"); let passphrase = content.passphrase.unwrap(); assert_eq!(passphrase.algorithm, KeyDerivationAlgorithm::Pbkfd2); assert_eq!(passphrase.salt, "rocksalt"); assert_eq!(passphrase.iterations, uint!(8)); assert_eq!(passphrase.bits, uint!(256)); let (iv, mac) = assert_matches!( content.algorithm, SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { iv, mac, } => (iv, mac) ); assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA"); assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"); } #[test] fn test_event_serialization() { let event = GlobalAccountDataEvent { content: SecretStorageKeyEventContent::new( "my_key_id".into(), "my_key".into(), SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), }, ), }; let json = json!({ "type": "m.secret_storage.key.my_key_id", "content": { "name": "my_key", "algorithm": "m.secret_storage.v1.aes-hmac-sha2", "iv": "YWJjZGVmZ2hpamtsbW5vcA", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" } }); assert_eq!(to_json_value(&event).unwrap(), json); } #[test] fn test_event_deserialization() { let json = json!({ "type": "m.secret_storage.key.my_key_id", "content": { "name": "my_key", "algorithm": "m.secret_storage.v1.aes-hmac-sha2", "iv": "YWJjZGVmZ2hpamtsbW5vcA", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" } }); let ev = from_json_value::>(json).unwrap(); assert_eq!(ev.content.key_id, "my_key_id"); assert_eq!(ev.content.name, "my_key"); assert_matches!(ev.content.passphrase, None); let (iv, mac) = assert_matches!( ev.content.algorithm, SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { iv, mac, } => (iv, mac) ); assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA"); assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"); } } ruma-common-0.10.5/src/events/secret_storage/secret.rs000064400000000000000000000070401046102023000211010ustar 00000000000000//! Types for events used for secrets to be stored in the user's account_data. use std::collections::BTreeMap; use crate::serde::Base64; use serde::{Deserialize, Serialize}; /// A secret and its encrypted contents. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SecretEventContent { /// Map from key ID to the encrypted data. /// /// The exact format for the encrypted data is dependent on the key algorithm. pub encrypted: BTreeMap, } impl SecretEventContent { /// Create a new `SecretEventContent` with the given encrypted content. pub fn new(encrypted: BTreeMap) -> Self { Self { encrypted } } } /// Encrypted data for a corresponding secret storage encryption algorithm. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(untagged)] pub enum SecretEncryptedData { /// Data encrypted using the *m.secret_storage.v1.aes-hmac-sha2* algorithm. AesHmacSha2EncryptedData { /// The 16-byte initialization vector, encoded as base64. iv: Base64, /// The AES-CTR-encrypted data, encoded as base64. ciphertext: Base64, /// The MAC, encoded as base64. mac: Base64, }, } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{SecretEncryptedData, SecretEventContent}; use crate::serde::Base64; #[test] fn test_secret_serialization() { let key_one_data = SecretEncryptedData::AesHmacSha2EncryptedData { iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), ciphertext: Base64::parse("dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ").unwrap(), mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), }; let mut encrypted = BTreeMap::::new(); encrypted.insert("key_one".to_owned(), key_one_data); let content = SecretEventContent::new(encrypted); let json = json!({ "encrypted": { "key_one" : { "iv": "YWJjZGVmZ2hpamtsbW5vcA", "ciphertext": "dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" } } }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn test_secret_deserialization() { let json = json!({ "encrypted": { "key_one" : { "iv": "YWJjZGVmZ2hpamtsbW5vcA", "ciphertext": "dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ", "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" } } }); let deserialized: SecretEventContent = from_json_value(json).unwrap(); let secret_data = deserialized.encrypted.get("key_one").unwrap(); let (iv, ciphertext, mac) = assert_matches!( secret_data, SecretEncryptedData::AesHmacSha2EncryptedData { iv, ciphertext, mac } => (iv, ciphertext, mac) ); assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA"); assert_eq!(ciphertext.encode(), "dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ"); assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"); } } ruma-common-0.10.5/src/events/secret_storage.rs000064400000000000000000000001561046102023000176150ustar 00000000000000//! Module for events in the `m.secret_storage` namespace. pub mod default_key; pub mod key; pub mod secret; ruma-common-0.10.5/src/events/space/child.rs000064400000000000000000000133561046102023000167700ustar 00000000000000//! Types for the [`m.space.child`] event. //! //! [`m.space.child`]: https://spec.matrix.org/v1.2/client-server-api/#mspacechild use ruma_macros::{Event, EventContent}; use serde::{Deserialize, Serialize}; use crate::{MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedServerName, OwnedUserId}; /// The content of an `m.space.child` event. /// /// The admins of a space can advertise rooms and subspaces for their space by setting /// `m.space.child` state events. /// /// The `state_key` is the ID of a child room or space, and the content must contain a `via` key /// which gives a list of candidate servers that can be used to join the room. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.space.child", kind = State, state_key_type = OwnedRoomId)] pub struct SpaceChildEventContent { /// List of candidate servers that can be used to join the room. #[serde(skip_serializing_if = "Option::is_none")] pub via: Option>, /// Provide a default ordering of siblings in the room list. /// /// Rooms are sorted based on a lexicographic ordering of the Unicode codepoints of the /// characters in `order` values. Rooms with no `order` come last, in ascending numeric order /// of the origin_server_ts of their m.room.create events, or ascending lexicographic order of /// their room_ids in case of equal `origin_server_ts`. `order`s which are not strings, or do /// not consist solely of ascii characters in the range `\x20` (space) to `\x7E` (`~`), or /// consist of more than 50 characters, are forbidden and the field should be ignored if /// received. #[serde(skip_serializing_if = "Option::is_none")] pub order: Option, /// Space admins can mark particular children of a space as "suggested". /// /// This mainly serves as a hint to clients that that they can be displayed differently, for /// example by showing them eagerly in the room list. A child which is missing the `suggested` /// property is treated identically to a child with `"suggested": false`. A suggested child may /// be a room or a subspace. #[serde(skip_serializing_if = "Option::is_none")] pub suggested: Option, } impl SpaceChildEventContent { /// Creates a new `ChildEventContent`. pub fn new() -> Self { Self::default() } } /// An `m.space.child` event represented as a Stripped State Event with an added `origin_server_ts` /// key. #[derive(Clone, Debug, Event)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct HierarchySpaceChildEvent { /// The content of the space child event. pub content: SpaceChildEventContent, /// The fully-qualified ID of the user who sent this event. pub sender: OwnedUserId, /// The room ID of the child. pub state_key: String, /// Timestamp in milliseconds on originating homeserver when this event was sent. pub origin_server_ts: MilliSecondsSinceUnixEpoch, } #[cfg(test)] mod tests { use js_int::uint; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{HierarchySpaceChildEvent, SpaceChildEventContent}; use crate::{server_name, user_id, MilliSecondsSinceUnixEpoch}; #[test] fn space_child_serialization() { let content = SpaceChildEventContent { via: Some(vec![server_name!("example.com").to_owned()]), order: Some("uwu".to_owned()), suggested: Some(false), }; let json = json!({ "via": ["example.com"], "order": "uwu", "suggested": false, }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn space_child_empty_serialization() { let content = SpaceChildEventContent { via: None, order: None, suggested: None }; let json = json!({}); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn hierarchy_space_child_serialization() { let event = HierarchySpaceChildEvent { content: SpaceChildEventContent { via: Some(vec![server_name!("example.com").to_owned()]), order: Some("uwu".to_owned()), suggested: None, }, sender: user_id!("@example:localhost").to_owned(), state_key: "!child:localhost".to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1_629_413_349)), }; let json = json!({ "content": { "via": ["example.com"], "order": "uwu", }, "sender": "@example:localhost", "state_key": "!child:localhost", "origin_server_ts": 1_629_413_349, "type": "m.space.child", }); assert_eq!(to_json_value(&event).unwrap(), json); } #[test] fn hierarchy_space_child_deserialization() { let json = json!({ "content": { "via": [ "example.org" ] }, "origin_server_ts": 1_629_413_349, "sender": "@alice:example.org", "state_key": "!a:example.org", "type": "m.space.child" }); let ev = from_json_value::(json).unwrap(); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_629_413_349))); assert_eq!(ev.sender, "@alice:example.org"); assert_eq!(ev.state_key, "!a:example.org"); let via = ev.content.via.unwrap(); assert_eq!(via.len(), 1); assert_eq!(via[0], "example.org"); assert_eq!(ev.content.order, None); assert_eq!(ev.content.suggested, None); } } ruma-common-0.10.5/src/events/space/parent.rs000064400000000000000000000050231046102023000171660ustar 00000000000000//! Types for the [`m.space.parent`] event. //! //! [`m.space.parent`]: https://spec.matrix.org/v1.2/client-server-api/#mspaceparent use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{OwnedRoomId, OwnedServerName}; /// The content of an `m.space.parent` event. /// /// Rooms can claim parents via the `m.space.parent` state event. /// /// Similar to `m.space.child`, the `state_key` is the ID of the parent space, and the content must /// contain a `via` key which gives a list of candidate servers that can be used to join the /// parent. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.space.parent", kind = State, state_key_type = OwnedRoomId)] pub struct SpaceParentEventContent { /// List of candidate servers that can be used to join the room. #[serde(skip_serializing_if = "Option::is_none")] pub via: Option>, /// Determines whether this is the main parent for the space. /// /// When a user joins a room with a canonical parent, clients may switch to view the room in /// the context of that space, peeking into it in order to find other rooms and group them /// together. In practice, well behaved rooms should only have one `canonical` parent, but /// given this is not enforced: if multiple are present the client should select the one with /// the lowest room ID, as determined via a lexicographic ordering of the Unicode code-points. pub canonical: bool, } impl SpaceParentEventContent { /// Creates a new `ParentEventContent` with the given canonical flag. pub fn new(canonical: bool) -> Self { Self { via: None, canonical } } } #[cfg(test)] mod tests { use serde_json::{json, to_value as to_json_value}; use super::SpaceParentEventContent; use crate::server_name; #[test] fn space_parent_serialization() { let content = SpaceParentEventContent { via: Some(vec![server_name!("example.com").to_owned()]), canonical: true, }; let json = json!({ "via": ["example.com"], "canonical": true, }); assert_eq!(to_json_value(&content).unwrap(), json); } #[test] fn space_parent_empty_serialization() { let content = SpaceParentEventContent { via: None, canonical: true }; let json = json!({ "canonical": true, }); assert_eq!(to_json_value(&content).unwrap(), json); } } ruma-common-0.10.5/src/events/space.rs000064400000000000000000000002351046102023000156750ustar 00000000000000//! Types for the `m.space` events. //! //! See [the specification](https://spec.matrix.org/v1.2/client-server-api/#spaces). pub mod child; pub mod parent; ruma-common-0.10.5/src/events/state_key.rs000064400000000000000000000017441046102023000166000ustar 00000000000000use serde::{ de::{self, Deserialize, Deserializer, Unexpected}, Serialize, Serializer, }; /// A type that can be used as the `state_key` for event types where that field is always empty. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(clippy::exhaustive_structs)] pub struct EmptyStateKey; impl AsRef for EmptyStateKey { fn as_ref(&self) -> &str { "" } } impl<'de> Deserialize<'de> for EmptyStateKey { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = crate::serde::deserialize_cow_str(deserializer)?; if s.is_empty() { Ok(EmptyStateKey) } else { Err(de::Error::invalid_value(Unexpected::Str(&s), &"an empty string")) } } } impl Serialize for EmptyStateKey { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str("") } } ruma-common-0.10.5/src/events/sticker.rs000064400000000000000000000102761046102023000162540ustar 00000000000000//! Types for the [`m.sticker`] event. //! //! [`m.sticker`]: https://spec.matrix.org/v1.2/client-server-api/#msticker use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-msc3552")] use super::{ file::{FileContent, FileContentInfo}, image::{ImageContent, ThumbnailContent}, message::MessageContent, }; use crate::{events::room::ImageInfo, OwnedMxcUri}; /// The content of an `m.sticker` event. /// /// A sticker message. /// /// With the `unstable-msc3552` feature, this type also contains the transitional extensible events /// format. See the documentation of the [`message`] module for more information. /// /// [`message`]: super::message #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.sticker", kind = MessageLike)] pub struct StickerEventContent { /// A textual representation or associated description of the sticker image. /// /// This could be the alt text of the original image, or a message to accompany and further /// describe the sticker. pub body: String, /// Metadata about the image referred to in `url` including a thumbnail representation. pub info: ImageInfo, /// The URL to the sticker image. pub url: OwnedMxcUri, /// Extensible-event text representation of the message. /// /// If present, this should be preferred over the `body` field. #[cfg(feature = "unstable-msc3552")] #[serde(flatten, skip_serializing_if = "Option::is_none")] pub message: Option, /// Extensible-event file content of the message. /// /// If present, this should be preferred over the `url`, `file` and `info` fields. #[cfg(feature = "unstable-msc3552")] #[serde( rename = "org.matrix.msc1767.file", alias = "m.file", skip_serializing_if = "Option::is_none" )] pub file: Option, /// Extensible-event image info of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3552")] #[serde( rename = "org.matrix.msc1767.image", alias = "m.image", skip_serializing_if = "Option::is_none" )] pub image: Option>, /// Extensible-event thumbnails of the message. /// /// If present, this should be preferred over the `info` field. #[cfg(feature = "unstable-msc3552")] #[serde( rename = "org.matrix.msc1767.thumbnail", alias = "m.thumbnail", skip_serializing_if = "Option::is_none" )] pub thumbnail: Option>, /// Extensible-event captions of the message. #[cfg(feature = "unstable-msc3552")] #[serde( rename = "org.matrix.msc1767.caption", alias = "m.caption", with = "super::message::content_serde::as_vec", default, skip_serializing_if = "Option::is_none" )] pub caption: Option, } impl StickerEventContent { /// Creates a new `StickerEventContent` with the given body, image info and URL. pub fn new(body: String, info: ImageInfo, url: OwnedMxcUri) -> Self { Self { #[cfg(feature = "unstable-msc3552")] message: Some(MessageContent::plain(body.clone())), #[cfg(feature = "unstable-msc3552")] file: Some(FileContent::plain( url.clone(), FileContentInfo::from_room_message_content(None, info.mimetype.clone(), info.size) .map(Box::new), )), #[cfg(feature = "unstable-msc3552")] image: Some(Box::new( ImageContent::from_room_message_content(info.width, info.height) .unwrap_or_default(), )), #[cfg(feature = "unstable-msc3552")] thumbnail: ThumbnailContent::from_room_message_content( info.thumbnail_source.clone(), info.thumbnail_info.clone(), ) .map(|thumbnail| vec![thumbnail]), #[cfg(feature = "unstable-msc3552")] caption: None, body, info, url, } } } ruma-common-0.10.5/src/events/tag.rs000064400000000000000000000142271046102023000153630ustar 00000000000000//! Types for the [`m.tag`] event. //! //! [`m.tag`]: https://spec.matrix.org/v1.2/client-server-api/#mtag use std::{collections::BTreeMap, error::Error, fmt, str::FromStr}; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::{serde::deserialize_cow_str, PrivOwnedStr}; /// Map of tag names to tag info. pub type Tags = BTreeMap; /// The content of an `m.tag` event. /// /// Informs the client of tags on a room. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.tag", kind = RoomAccountData)] pub struct TagEventContent { /// A map of tag names to tag info. pub tags: Tags, } impl TagEventContent { /// Creates a new `TagEventContent` with the given `Tags`. pub fn new(tags: Tags) -> Self { Self { tags } } } impl From for TagEventContent { fn from(tags: Tags) -> Self { Self::new(tags) } } /// A user-defined tag name. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct UserTagName { name: String, } impl AsRef for UserTagName { fn as_ref(&self) -> &str { &self.name } } impl FromStr for UserTagName { type Err = InvalidUserTagName; fn from_str(s: &str) -> Result { if s.starts_with("u.") { Ok(Self { name: s.into() }) } else { Err(InvalidUserTagName) } } } /// An error returned when attempting to create a UserTagName with a string that would make it /// invalid. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct InvalidUserTagName; impl fmt::Display for InvalidUserTagName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "missing 'u.' prefix in UserTagName") } } impl Error for InvalidUserTagName {} /// The name of a tag. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum TagName { /// `m.favourite`: The user's favorite rooms. /// /// These should be shown with higher precedence than other rooms. Favorite, /// `m.lowpriority`: These should be shown with lower precedence than others. LowPriority, /// `m.server_notice`: Used to identify /// [Server Notice Rooms](https://spec.matrix.org/v1.2/client-server-api/#server-notices). ServerNotice, /// `u.*`: User-defined tag User(UserTagName), /// A custom tag #[doc(hidden)] _Custom(PrivOwnedStr), } impl TagName { /// Returns the display name of the tag. /// /// That means the string after `m.` or `u.` for spec- and user-defined tag names, and the /// string after the last dot for custom tags. If no dot is found, returns the whole string. pub fn display_name(&self) -> &str { match self { Self::_Custom(s) => { let start = s.0.rfind('.').map(|p| p + 1).unwrap_or(0); &self.as_ref()[start..] } _ => &self.as_ref()[2..], } } } impl AsRef for TagName { fn as_ref(&self) -> &str { match self { Self::Favorite => "m.favourite", Self::LowPriority => "m.lowpriority", Self::ServerNotice => "m.server_notice", Self::User(tag) => tag.as_ref(), Self::_Custom(s) => &s.0, } } } impl From for TagName where T: AsRef + Into, { fn from(s: T) -> TagName { match s.as_ref() { "m.favourite" => Self::Favorite, "m.lowpriority" => Self::LowPriority, "m.server_notice" => Self::ServerNotice, s if s.starts_with("u.") => Self::User(UserTagName { name: s.into() }), s => Self::_Custom(PrivOwnedStr(s.into())), } } } impl fmt::Display for TagName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_ref().fmt(f) } } impl<'de> Deserialize<'de> for TagName { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let cow = deserialize_cow_str(deserializer)?; Ok(cow.into()) } } impl Serialize for TagName { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(self.as_ref()) } } /// Information about a tag. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct TagInfo { /// Value to use for lexicographically ordering rooms with this tag. #[serde(skip_serializing_if = "Option::is_none")] pub order: Option, } impl TagInfo { /// Creates an empty `TagInfo`. pub fn new() -> Self { Default::default() } } #[cfg(test)] mod tests { use maplit::btreemap; use serde_json::{json, to_value as to_json_value}; use super::{TagEventContent, TagInfo, TagName}; #[test] fn serialization() { let tags = btreemap! { TagName::Favorite => TagInfo::new(), TagName::LowPriority => TagInfo::new(), TagName::ServerNotice => TagInfo::new(), "u.custom".to_owned().into() => TagInfo { order: Some(0.9) } }; let content = TagEventContent { tags }; assert_eq!( to_json_value(content).unwrap(), json!({ "tags": { "m.favourite": {}, "m.lowpriority": {}, "m.server_notice": {}, "u.custom": { "order": 0.9 } }, }) ); } #[test] fn display_name() { assert_eq!(TagName::Favorite.display_name(), "favourite"); assert_eq!(TagName::LowPriority.display_name(), "lowpriority"); assert_eq!(TagName::ServerNotice.display_name(), "server_notice"); assert_eq!(TagName::from("u.Work").display_name(), "Work"); assert_eq!(TagName::from("rs.conduit.rules").display_name(), "rules"); assert_eq!(TagName::from("Play").display_name(), "Play"); } } ruma-common-0.10.5/src/events/typing.rs000064400000000000000000000014731046102023000161210ustar 00000000000000//! Types for the [`m.typing`] event. //! //! [`m.typing`]: https://spec.matrix.org/v1.2/client-server-api/#mtyping use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use crate::OwnedUserId; /// The content of an `m.typing` event. /// /// Informs the client who is currently typing in a given room. #[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.typing", kind = EphemeralRoom)] pub struct TypingEventContent { /// The list of user IDs typing in this room, if any. pub user_ids: Vec, } impl TypingEventContent { /// Creates a new `TypingEventContent` with the given user IDs. pub fn new(user_ids: Vec) -> Self { Self { user_ids } } } ruma-common-0.10.5/src/events/unsigned.rs000064400000000000000000000155071046102023000164260ustar 00000000000000use js_int::Int; use serde::{Deserialize, Serialize}; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; use super::{relation::Relations, room::redaction::SyncRoomRedactionEvent, StateEventContent}; use crate::{ serde::{CanBeEmpty, Raw}, OwnedTransactionId, }; /// Extra information about a message event that is not incorporated into the event's hash. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct MessageLikeUnsigned { /// The time in milliseconds that has elapsed since the event was sent. /// /// This field is generated by the local homeserver, and may be incorrect if the local time on /// at least one of the two servers is out of sync, which can cause the age to either be /// negative or greater than it actually is. #[serde(skip_serializing_if = "Option::is_none")] pub age: Option, /// The client-supplied transaction ID, if the client being given the event is the same one /// which sent it. #[serde(skip_serializing_if = "Option::is_none")] pub transaction_id: Option, /// [Bundled aggregations] of related child events. /// /// [Bundled aggregations]: https://spec.matrix.org/v1.3/client-server-api/#aggregations #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")] pub relations: Option, } impl MessageLikeUnsigned { /// Create a new `Unsigned` with fields set to `None`. pub fn new() -> Self { Self::default() } } impl CanBeEmpty for MessageLikeUnsigned { /// Whether this unsigned data is empty (all fields are `None`). /// /// This method is used to determine whether to skip serializing the `unsigned` field in room /// events. Do not use it to determine whether an incoming `unsigned` field was present - it /// could still have been present but contained none of the known fields. fn is_empty(&self) -> bool { self.age.is_none() && self.transaction_id.is_none() && self.relations.is_none() } } /// Extra information about a state event that is not incorporated into the event's hash. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct StateUnsigned { /// The time in milliseconds that has elapsed since the event was sent. /// /// This field is generated by the local homeserver, and may be incorrect if the local time on /// at least one of the two servers is out of sync, which can cause the age to either be /// negative or greater than it actually is. #[serde(skip_serializing_if = "Option::is_none")] pub age: Option, /// The client-supplied transaction ID, if the client being given the event is the same one /// which sent it. #[serde(skip_serializing_if = "Option::is_none")] pub transaction_id: Option, /// Optional previous content of the event. #[serde(skip_serializing_if = "Option::is_none")] pub prev_content: Option, /// [Bundled aggregations] of related child events. /// /// [Bundled aggregations]: https://spec.matrix.org/v1.3/client-server-api/#aggregations #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")] pub relations: Option, } impl StateUnsigned { /// Create a new `Unsigned` with fields set to `None`. pub fn new() -> Self { Self { age: None, transaction_id: None, prev_content: None, relations: None } } } impl CanBeEmpty for StateUnsigned { /// Whether this unsigned data is empty (all fields are `None`). /// /// This method is used to determine whether to skip serializing the `unsigned` field in room /// events. Do not use it to determine whether an incoming `unsigned` field was present - it /// could still have been present but contained none of the known fields. fn is_empty(&self) -> bool { self.age.is_none() && self.transaction_id.is_none() && self.prev_content.is_none() && self.relations.is_none() } } /// Helper functions for proc-macro code. /// /// Needs to be public for state events defined outside ruma-common. #[doc(hidden)] pub trait StateUnsignedFromParts: Sized { fn _from_parts(event_type: &str, object: &RawJsonValue) -> serde_json::Result; } impl StateUnsignedFromParts for StateUnsigned { fn _from_parts(event_type: &str, object: &RawJsonValue) -> serde_json::Result { #[derive(Deserialize)] #[serde(bound = "")] // Disable default C: Deserialize bound struct WithRawPrevContent { #[serde(skip_serializing_if = "Option::is_none")] age: Option, #[serde(skip_serializing_if = "Option::is_none")] transaction_id: Option, prev_content: Option>, #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")] relations: Option, } let raw: WithRawPrevContent = from_json_str(object.get())?; let prev_content = raw.prev_content.map(|r| r.deserialize_content(event_type.into())).transpose()?; Ok(Self { age: raw.age, transaction_id: raw.transaction_id, relations: raw.relations, prev_content, }) } } impl Default for StateUnsigned { fn default() -> Self { Self::new() } } /// Extra information about a redacted event that is not incorporated into the event's hash. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RedactedUnsigned { /// The event that redacted this event, if any. #[serde(skip_serializing_if = "Option::is_none")] pub redacted_because: Option>, } impl RedactedUnsigned { /// Create a new `RedactedUnsigned` with field set to `None`. pub fn new() -> Self { Self::default() } /// Create a new `RedactedUnsigned` with the given redacted because. pub fn new_because(redacted_because: Box) -> Self { Self { redacted_because: Some(redacted_because) } } } impl CanBeEmpty for RedactedUnsigned { /// Whether this unsigned data is empty (`redacted_because` is `None`). /// /// This method is used to determine whether to skip serializing the `unsigned` field in /// redacted room events. Do not use it to determine whether an incoming `unsigned` field /// was present - it could still have been present but contained none of the known fields. fn is_empty(&self) -> bool { self.redacted_because.is_none() } } ruma-common-0.10.5/src/events/video.rs000064400000000000000000000137671046102023000157260ustar 00000000000000//! Types for extensible video message events ([MSC3553]). //! //! [MSC3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 use std::time::Duration; use js_int::UInt; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ file::FileContent, image::ThumbnailContent, message::MessageContent, room::message::{ MessageType, Relation, RoomMessageEventContent, VideoInfo, VideoMessageEventContent, }, }; /// The payload for an extensible video message. /// /// This is the new primary type introduced in [MSC3553] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `VideoEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Video`]. You can convert it back with /// [`VideoEventContent::from_video_room_message()`]. /// /// [MSC3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Video`]: super::room::message::MessageType::Video #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.video", kind = MessageLike)] pub struct VideoEventContent { /// The text representation of the message. #[serde(flatten)] pub message: MessageContent, /// The file content of the message. #[serde(rename = "m.file")] pub file: FileContent, /// The video content of the message. #[serde(rename = "m.video")] pub video: Box, /// The thumbnails of the message. #[serde(rename = "m.thumbnail", default, skip_serializing_if = "Vec::is_empty")] pub thumbnail: Vec, /// The captions of the message. #[serde( rename = "m.caption", with = "super::message::content_serde::as_vec", default, skip_serializing_if = "Option::is_none" )] pub caption: Option, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl VideoEventContent { /// Creates a new `VideoEventContent` with the given plain text message and file. pub fn plain(message: impl Into, file: FileContent) -> Self { Self { message: MessageContent::plain(message), file, video: Default::default(), thumbnail: Default::default(), caption: Default::default(), relates_to: None, } } /// Creates a new `VideoEventContent` with the given message and file. pub fn with_message(message: MessageContent, file: FileContent) -> Self { Self { message, file, video: Default::default(), thumbnail: Default::default(), caption: Default::default(), relates_to: None, } } /// Create a new `VideoEventContent` from the given `VideoMessageEventContent` and optional /// relation. pub fn from_video_room_message( content: VideoMessageEventContent, relates_to: Option, ) -> Self { let VideoMessageEventContent { body, source, info, message, file, video, thumbnail, caption, } = content; let VideoInfo { duration, height, width, mimetype, size, thumbnail_info, thumbnail_source, .. } = info.map(|info| *info).unwrap_or_default(); let message = message.unwrap_or_else(|| MessageContent::plain(body)); let file = file.unwrap_or_else(|| { FileContent::from_room_message_content(source, None, mimetype, size) }); let video = video.unwrap_or_else(|| { Box::new(VideoContent::from_room_message_content(height, width, duration)) }); let thumbnail = thumbnail.unwrap_or_else(|| { ThumbnailContent::from_room_message_content(thumbnail_source, thumbnail_info) .into_iter() .collect() }); Self { message, file, video, thumbnail, caption, relates_to } } } impl From for RoomMessageEventContent { fn from(content: VideoEventContent) -> Self { let VideoEventContent { message, file, video, thumbnail, caption, relates_to } = content; Self { msgtype: MessageType::Video(VideoMessageEventContent::from_extensible_content( message, file, video, thumbnail, caption, )), relates_to, } } } /// Video content. #[derive(Default, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct VideoContent { /// The height of the video in pixels. #[serde(skip_serializing_if = "Option::is_none")] pub height: Option, /// The width of the video in pixels. #[serde(skip_serializing_if = "Option::is_none")] pub width: Option, /// The duration of the video in milliseconds. #[serde( with = "crate::serde::duration::opt_ms", default, skip_serializing_if = "Option::is_none" )] pub duration: Option, } impl VideoContent { /// Creates a new empty `VideoContent`. pub fn new() -> Self { Self::default() } /// Creates a new `VideoContent` with the given optional height, width and duration. pub(crate) fn from_room_message_content( height: Option, width: Option, duration: Option, ) -> Self { Self { height, width, duration } } /// Whether this `VideoContent` is empty. pub fn is_empty(&self) -> bool { self.height.is_none() && self.width.is_none() && self.duration.is_none() } } ruma-common-0.10.5/src/events/voice.rs000064400000000000000000000111261046102023000157100ustar 00000000000000//! Types for voice message events ([MSC3245]). //! //! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use super::{ audio::AudioContent, file::FileContent, message::{MessageContent, TryFromExtensibleError}, room::message::{ AudioInfo, AudioMessageEventContent, MessageType, Relation, RoomMessageEventContent, }, }; /// The payload for an extensible voice message. /// /// This is the new primary type introduced in [MSC3245] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// `VoiceEventContent` can be converted to a [`RoomMessageEventContent`] with a /// [`MessageType::Audio`] with the `m.voice` flag. You can convert it back with /// [`VoiceEventContent::try_from_audio_room_message()`]. /// /// [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 /// [`message`]: super::message /// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent /// [`MessageType::Audio`]: super::room::message::MessageType::Audio #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.voice", kind = MessageLike)] pub struct VoiceEventContent { /// The text representation of the message. #[serde(flatten)] pub message: MessageContent, /// The file content of the message. #[serde(rename = "m.file")] pub file: FileContent, /// The audio content of the message. #[serde(rename = "m.audio")] pub audio: AudioContent, /// The voice content of the message. #[serde(rename = "m.voice")] pub voice: VoiceContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl VoiceEventContent { /// Creates a new `VoiceEventContent` with the given plain text representation and file. pub fn plain(message: impl Into, file: FileContent) -> Self { Self { message: MessageContent::plain(message), file, audio: Default::default(), voice: Default::default(), relates_to: None, } } /// Creates a new `VoiceEventContent` with the given message and file. pub fn with_message(message: MessageContent, file: FileContent) -> Self { Self { message, file, audio: Default::default(), voice: Default::default(), relates_to: None, } } /// Create a new `VoiceEventContent` from the given `AudioMessageEventContent` and optional /// relation. /// /// This can fail if the `AudioMessageEventContent` is not a voice message. pub fn try_from_audio_room_message( content: AudioMessageEventContent, relates_to: Option, ) -> Result { let AudioMessageEventContent { body, source, info, message, file, audio, voice } = content; let AudioInfo { duration, mimetype, size } = info.map(|info| *info).unwrap_or_default(); let message = message.unwrap_or_else(|| MessageContent::plain(body)); let file = file.unwrap_or_else(|| { FileContent::from_room_message_content(source, None, mimetype, size) }); let audio = audio .or_else(|| duration.map(AudioContent::from_room_message_content)) .unwrap_or_default(); let voice = if let Some(voice) = voice { voice } else { return Err(TryFromExtensibleError::MissingField("m.voice".to_owned())); }; Ok(Self { message, file, audio, voice, relates_to }) } } impl From for RoomMessageEventContent { fn from(content: VoiceEventContent) -> Self { let VoiceEventContent { message, file, audio, voice, relates_to } = content; Self { msgtype: MessageType::Audio(AudioMessageEventContent::from_extensible_voice_content( message, file, audio, voice, )), relates_to, } } } /// Voice content. /// /// This is currently empty and used as a flag to mark an audio event that should be displayed as a /// voice message. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct VoiceContent {} impl VoiceContent { /// Creates a new empty `VoiceContent`. pub fn new() -> Self { Self::default() } } ruma-common-0.10.5/src/events.rs000064400000000000000000000176621046102023000146160ustar 00000000000000//! (De)serializable types for the events in the [Matrix](https://matrix.org) specification. //! These types are used by other Ruma crates. //! //! All data exchanged over Matrix is expressed as an event. //! Different event types represent different actions, such as joining a room or sending a message. //! Events are stored and transmitted as simple JSON structures. //! While anyone can create a new event type for their own purposes, the Matrix specification //! defines a number of event types which are considered core to the protocol. //! This module contains Rust types for all of the event types defined by the specification and //! facilities for extending the event system for custom event types. //! //! # Core event types //! //! This module includes Rust types for all event types in the Matrix specification. //! To better organize the crate, these types live in separate modules with a hierarchy that matches //! the reverse domain name notation of the event type. For example, the `m.room.message` event //! lives at `ruma::events::room::message::RoomMessageEvent`. Each type's module also contains a //! Rust type for that event type's `content` field, and any other supporting types required by the //! event's other fields. //! //! # Extending Ruma with custom events //! //! For our examples we will start with a simple custom state event. `ruma_event` //! specifies the state event's `type` and it's [`kind`](EventKind). //! //! ```rust //! use ruma_common::events::macros::EventContent; //! use serde::{Deserialize, Serialize}; //! //! #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] //! #[ruma_event(type = "org.example.event", kind = State, state_key_type = String)] //! pub struct ExampleContent { //! field: String, //! } //! ``` //! //! This can be used with events structs, such as passing it into //! `ruma::api::client::state::send_state_event`'s `Request`. //! //! As a more advanced example we create a reaction message event. For this event we will use a //! [`OriginalSyncMessageLikeEvent`] struct but any [`OriginalMessageLikeEvent`] struct would work. //! //! ```rust //! use ruma_common::{ //! events::{macros::EventContent, OriginalSyncMessageLikeEvent}, //! OwnedEventId, //! }; //! use serde::{Deserialize, Serialize}; //! //! #[derive(Clone, Debug, Deserialize, Serialize)] //! #[serde(tag = "rel_type")] //! pub enum RelatesTo { //! #[serde(rename = "m.annotation")] //! Annotation { //! /// The event this reaction relates to. //! event_id: OwnedEventId, //! /// The displayable content of the reaction. //! key: String, //! }, //! //! /// Since this event is not fully specified in the Matrix spec //! /// it may change or types may be added, we are ready! //! #[serde(rename = "m.whatever")] //! Whatever, //! } //! //! /// The payload for our reaction event. //! #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] //! #[ruma_event(type = "m.reaction", kind = MessageLike)] //! pub struct ReactionEventContent { //! #[serde(rename = "m.relates_to")] //! pub relates_to: RelatesTo, //! } //! //! let json = serde_json::json!({ //! "content": { //! "m.relates_to": { //! "event_id": "$xxxx-xxxx", //! "key": "👍", //! "rel_type": "m.annotation" //! } //! }, //! "event_id": "$xxxx-xxxx", //! "origin_server_ts": 1, //! "sender": "@someone:example.org", //! "type": "m.reaction", //! "unsigned": { //! "age": 85 //! } //! }); //! //! // The downside of this event is we cannot use it with event enums, //! // but could be deserialized from a `Raw<_>` that has failed to deserialize. //! assert_matches::assert_matches!( //! serde_json::from_value::>(json), //! Ok(OriginalSyncMessageLikeEvent { //! content: ReactionEventContent { //! relates_to: RelatesTo::Annotation { key, .. }, //! }, //! .. //! }) if key == "👍" //! ); //! ``` use serde::{de::IgnoredAny, Deserialize, Serializer}; use self::room::redaction::SyncRoomRedactionEvent; use crate::{EventEncryptionAlgorithm, RoomVersionId}; // Needs to be public for trybuild tests #[doc(hidden)] pub mod _custom; mod content; mod enums; mod kinds; mod state_key; mod unsigned; /// Re-export of all the derives needed to create your own event types. pub mod macros { pub use ruma_macros::{Event, EventContent}; } #[cfg(feature = "unstable-msc3246")] pub mod audio; pub mod call; pub mod direct; pub mod dummy; #[cfg(feature = "unstable-msc1767")] pub mod emote; #[cfg(feature = "unstable-msc3551")] pub mod file; pub mod forwarded_room_key; pub mod fully_read; pub mod identity_server; pub mod ignored_user_list; #[cfg(feature = "unstable-msc3552")] pub mod image; pub mod key; #[cfg(feature = "unstable-msc3488")] pub mod location; #[cfg(feature = "unstable-msc1767")] pub mod message; #[cfg(feature = "unstable-msc1767")] pub mod notice; #[cfg(feature = "unstable-pdu")] pub mod pdu; pub mod policy; #[cfg(feature = "unstable-msc3381")] pub mod poll; pub mod presence; pub mod push_rules; #[cfg(feature = "unstable-msc2677")] pub mod reaction; pub mod receipt; pub mod relation; pub mod room; pub mod room_key; pub mod room_key_request; pub mod secret; pub mod secret_storage; pub mod space; pub mod sticker; pub mod tag; pub mod typing; #[cfg(feature = "unstable-msc3553")] pub mod video; #[cfg(feature = "unstable-msc3245")] pub mod voice; pub use self::{ content::*, enums::*, kinds::*, relation::Relations, state_key::EmptyStateKey, unsigned::{MessageLikeUnsigned, RedactedUnsigned, StateUnsigned, StateUnsignedFromParts}, }; /// Trait to define the behavior of redacting an event. pub trait Redact { /// The redacted form of the event. type Redacted; /// Transforms `self` into a redacted form (removing most fields) according to the spec. /// /// A small number of events have room-version specific redaction behavior, so a version has to /// be specified. fn redact(self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) -> Self::Redacted; } /// Trait to define the behavior of redact an event's content object. pub trait RedactContent { /// The redacted form of the event's content. type Redacted; /// Transform `self` into a redacted form (removing most or all fields) according to the spec. /// /// A small number of events have room-version specific redaction behavior, so a version has to /// be specified. /// /// Where applicable, it is preferred to use [`Redact::redact`] on the outer event. fn redact(self, version: &RoomVersionId) -> Self::Redacted; } /// Helper struct to determine the event kind from a `serde_json::value::RawValue`. #[doc(hidden)] #[derive(Deserialize)] #[allow(clippy::exhaustive_structs)] pub struct EventTypeDeHelper<'a> { #[serde(borrow, rename = "type")] pub ev_type: std::borrow::Cow<'a, str>, } /// Helper struct to determine if an event has been redacted. #[doc(hidden)] #[derive(Deserialize)] #[allow(clippy::exhaustive_structs)] pub struct RedactionDeHelper { /// Used to check whether redacted_because exists. pub unsigned: Option, } #[doc(hidden)] #[derive(Deserialize)] #[allow(clippy::exhaustive_structs)] pub struct UnsignedDeHelper { /// This is the field that signals an event has been redacted. pub redacted_because: Option, } /// Helper function for erroring when trying to serialize an event enum _Custom variant that can /// only be created by deserializing from an unknown event type. #[doc(hidden)] #[allow(clippy::ptr_arg)] pub fn serialize_custom_event_error(_: &T, _: S) -> Result { Err(serde::ser::Error::custom( "Failed to serialize event [content] enum: Unknown event type.\n\ To send custom events, turn them into `Raw` by going through `serde_json::value::to_raw_value` and `Raw::from_json`.", )) } ruma-common-0.10.5/src/identifiers/client_secret.rs000064400000000000000000000024111046102023000204240ustar 00000000000000//! Client secret identifier. use ruma_macros::IdZst; /// A client secret. /// /// Client secrets in Matrix are opaque character sequences of `[0-9a-zA-Z.=_-]`. Their length must /// must not exceed 255 characters. /// /// You can create one from a string (using `ClientSecret::parse()`) but the recommended way is to /// use `ClientSecret::new()` to generate a random one. If that function is not available for you, /// you need to activate this crate's `rand` Cargo feature. #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::client_secret::validate)] pub struct ClientSecret(str); impl ClientSecret { /// Creates a random client secret. /// /// This will currently be a UUID without hyphens, but no guarantees are made about the /// structure of client secrets generated from this function. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new() -> OwnedClientSecret { let id = uuid::Uuid::new_v4(); ClientSecret::from_borrowed(&id.simple().to_string()).to_owned() } } #[cfg(test)] mod tests { use super::ClientSecret; #[test] fn valid_secret() { <&ClientSecret>::try_from("this_=_a_valid_secret_1337").unwrap(); } } ruma-common-0.10.5/src/identifiers/crypto_algorithms.rs000064400000000000000000000065541046102023000213660ustar 00000000000000//! Key algorithms used in Matrix spec. use ruma_macros::StringEnum; use crate::PrivOwnedStr; /// The basic key algorithms in the specification. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] #[ruma_enum(rename_all = "snake_case")] pub enum DeviceKeyAlgorithm { /// The Ed25519 signature algorithm. Ed25519, /// The Curve25519 ECDH algorithm. Curve25519, /// The Curve25519 ECDH algorithm, but the key also contains signatures SignedCurve25519, #[doc(hidden)] _Custom(PrivOwnedStr), } /// The signing key algorithms defined in the Matrix spec. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] #[ruma_enum(rename_all = "snake_case")] pub enum SigningKeyAlgorithm { /// The Ed25519 signature algorithm. Ed25519, #[doc(hidden)] _Custom(PrivOwnedStr), } /// An encryption algorithm to be used to encrypt messages sent to a room. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] pub enum EventEncryptionAlgorithm { /// Olm version 1 using Curve25519, AES-256, and SHA-256. #[ruma_enum(rename = "m.olm.v1.curve25519-aes-sha2")] OlmV1Curve25519AesSha2, /// Megolm version 1 using AES-256 and SHA-256. #[ruma_enum(rename = "m.megolm.v1.aes-sha2")] MegolmV1AesSha2, #[doc(hidden)] _Custom(PrivOwnedStr), } /// A key algorithm to be used to generate a key from a passphrase. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] pub enum KeyDerivationAlgorithm { /// PBKDF2 #[ruma_enum(rename = "m.pbkdf2")] Pbkfd2, #[doc(hidden)] _Custom(PrivOwnedStr), } #[cfg(test)] mod tests { use super::{DeviceKeyAlgorithm, SigningKeyAlgorithm}; #[test] fn parse_device_key_algorithm() { assert_eq!(DeviceKeyAlgorithm::from("ed25519"), DeviceKeyAlgorithm::Ed25519); assert_eq!(DeviceKeyAlgorithm::from("curve25519"), DeviceKeyAlgorithm::Curve25519); assert_eq!( DeviceKeyAlgorithm::from("signed_curve25519"), DeviceKeyAlgorithm::SignedCurve25519 ); } #[test] fn parse_signing_key_algorithm() { assert_eq!(SigningKeyAlgorithm::from("ed25519"), SigningKeyAlgorithm::Ed25519); } #[test] fn event_encryption_algorithm_serde() { use serde_json::json; use super::EventEncryptionAlgorithm; use crate::serde::test::serde_json_eq; serde_json_eq(EventEncryptionAlgorithm::MegolmV1AesSha2, json!("m.megolm.v1.aes-sha2")); serde_json_eq( EventEncryptionAlgorithm::OlmV1Curve25519AesSha2, json!("m.olm.v1.curve25519-aes-sha2"), ); serde_json_eq(EventEncryptionAlgorithm::from("io.ruma.test"), json!("io.ruma.test")); } #[test] fn key_derivation_algorithm_serde() { use serde_json::json; use super::KeyDerivationAlgorithm; use crate::serde::test::serde_json_eq; serde_json_eq(KeyDerivationAlgorithm::Pbkfd2, json!("m.pbkdf2")); } } ruma-common-0.10.5/src/identifiers/device_id.rs000064400000000000000000000035141046102023000175210ustar 00000000000000use ruma_macros::IdZst; #[cfg(feature = "rand")] use super::generate_localpart; /// A Matrix key ID. /// /// Device identifiers in Matrix are completely opaque character sequences. This type is provided /// simply for its semantic value. /// /// # Example /// /// ``` /// use ruma_common::{device_id, DeviceId, OwnedDeviceId}; /// /// # #[cfg(feature = "rand")] { /// let random_id = DeviceId::new(); /// assert_eq!(random_id.as_str().len(), 8); /// # } /// /// let static_id = device_id!("01234567"); /// assert_eq!(static_id.as_str(), "01234567"); /// /// let ref_id: &DeviceId = "abcdefghi".into(); /// assert_eq!(ref_id.as_str(), "abcdefghi"); /// /// let owned_id: OwnedDeviceId = "ijklmnop".into(); /// assert_eq!(owned_id.as_str(), "ijklmnop"); /// ``` #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] pub struct DeviceId(str); impl DeviceId { /// Generates a random `DeviceId`, suitable for assignment to a new device. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new() -> OwnedDeviceId { Self::from_borrowed(&generate_localpart(8)).to_owned() } } #[cfg(all(test, feature = "rand"))] mod tests { use super::{DeviceId, OwnedDeviceId}; #[test] fn generate_device_id() { assert_eq!(DeviceId::new().as_str().len(), 8); } #[test] fn create_device_id_from_str() { let ref_id: &DeviceId = "abcdefgh".into(); assert_eq!(ref_id.as_str(), "abcdefgh"); } #[test] fn create_boxed_device_id_from_str() { let box_id: OwnedDeviceId = "12345678".into(); assert_eq!(box_id.as_str(), "12345678"); } #[test] fn create_device_id_from_box() { let box_str: Box = "ijklmnop".into(); let device_id: OwnedDeviceId = box_str.into(); assert_eq!(device_id.as_str(), "ijklmnop"); } } ruma-common-0.10.5/src/identifiers/device_key_id.rs000064400000000000000000000061031046102023000203660ustar 00000000000000//! Identifiers for device keys for end-to-end encryption. use ruma_macros::IdZst; use super::{crypto_algorithms::DeviceKeyAlgorithm, DeviceId}; /// A key algorithm and a device id, combined with a ':'. #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::device_key_id::validate)] pub struct DeviceKeyId(str); impl DeviceKeyId { /// Create a `DeviceKeyId` from a `DeviceKeyAlgorithm` and a `DeviceId`. pub fn from_parts(algorithm: DeviceKeyAlgorithm, device_id: &DeviceId) -> OwnedDeviceKeyId { let algorithm: &str = algorithm.as_ref(); let device_id: &str = device_id.as_ref(); let mut res = String::with_capacity(algorithm.len() + 1 + device_id.len()); res.push_str(algorithm); res.push(':'); res.push_str(device_id); Self::from_borrowed(&res).to_owned() } /// Returns key algorithm of the device key ID. pub fn algorithm(&self) -> DeviceKeyAlgorithm { self.as_str()[..self.colon_idx()].into() } /// Returns device ID of the device key ID. pub fn device_id(&self) -> &DeviceId { self.as_str()[self.colon_idx() + 1..].into() } fn colon_idx(&self) -> usize { self.as_str().find(':').unwrap() } } #[cfg(test)] mod tests { use super::{DeviceKeyId, OwnedDeviceKeyId}; use crate::identifiers::{crypto_algorithms::DeviceKeyAlgorithm, IdParseError}; #[test] fn convert_device_key_id() { assert_eq!( <&DeviceKeyId>::try_from("ed25519:JLAFKJWSCS") .expect("Failed to create device key ID."), "ed25519:JLAFKJWSCS" ); } #[test] fn serialize_device_key_id() { let device_key_id = <&DeviceKeyId>::try_from("ed25519:JLAFKJWSCS").unwrap(); let serialized = serde_json::to_value(device_key_id).unwrap(); assert_eq!(serialized, serde_json::json!("ed25519:JLAFKJWSCS")); } #[test] fn deserialize_device_key_id() { let deserialized: OwnedDeviceKeyId = serde_json::from_value(serde_json::json!("ed25519:JLAFKJWSCS")).unwrap(); let expected = <&DeviceKeyId>::try_from("ed25519:JLAFKJWSCS").unwrap(); assert_eq!(deserialized, expected); } #[test] fn missing_key_algorithm() { assert_eq!(<&DeviceKeyId>::try_from(":JLAFKJWSCS").unwrap_err(), IdParseError::Empty); } #[test] fn missing_delimiter() { assert_eq!( <&DeviceKeyId>::try_from("ed25519|JLAFKJWSCS").unwrap_err(), IdParseError::MissingColon, ); } #[test] fn empty_device_id_ok() { <&DeviceKeyId>::try_from("ed25519:").unwrap(); } #[test] fn valid_key_algorithm() { let device_key_id = <&DeviceKeyId>::try_from("ed25519:JLAFKJWSCS").unwrap(); assert_eq!(device_key_id.algorithm(), DeviceKeyAlgorithm::Ed25519); } #[test] fn valid_device_id() { let device_key_id = <&DeviceKeyId>::try_from("ed25519:JLAFKJWSCS").unwrap(); assert_eq!(device_key_id.device_id(), "JLAFKJWSCS"); } } ruma-common-0.10.5/src/identifiers/event_id.rs000064400000000000000000000177071046102023000174140ustar 00000000000000//! Matrix event identifiers. use ruma_macros::IdZst; use super::ServerName; /// A Matrix [event ID]. /// /// An `EventId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// /// # Room versions /// /// Matrix specifies multiple [room versions](https://spec.matrix.org/v1.2/#room-versions) and the /// format of event identifiers differ between them. The original format used by room versions 1 and /// 2 uses a short pseudorandom "localpart" followed by the hostname and port of the originating /// homeserver. Later room versions change event identifiers to be a hash of the event encoded with /// Base64. Some of the methods provided by `EventId` are only relevant to the original event /// format. /// /// ``` /// # use ruma_common::EventId; /// // Original format /// assert_eq!(<&EventId>::try_from("$h29iv0s8:example.com").unwrap(), "$h29iv0s8:example.com"); /// // Room version 3 format /// assert_eq!( /// <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(), /// "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" /// ); /// // Room version 4 format /// assert_eq!( /// <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(), /// "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" /// ); /// ``` /// /// [event ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::event_id::validate)] pub struct EventId(str); impl EventId { /// Attempts to generate an `EventId` for the given origin server with a localpart consisting /// of 18 random ASCII characters. /// /// This should only be used for events in the original format as used by Matrix room versions /// 1 and 2. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new(server_name: &ServerName) -> OwnedEventId { Self::from_borrowed(&format!("${}:{}", super::generate_localpart(18), server_name)) .to_owned() } /// Returns the event's unique ID. /// /// For the original event format as used by Matrix room versions 1 and 2, this is the /// "localpart" that precedes the homeserver. For later formats, this is the entire ID without /// the leading `$` sigil. pub fn localpart(&self) -> &str { let idx = self.colon_idx().unwrap_or_else(|| self.as_str().len()); &self.as_str()[1..idx] } /// Returns the server name of the event ID. /// /// Only applicable to events in the original format as used by Matrix room versions 1 and 2. pub fn server_name(&self) -> Option<&ServerName> { self.colon_idx().map(|idx| ServerName::from_borrowed(&self.as_str()[idx + 1..])) } fn colon_idx(&self) -> Option { self.as_str().find(':') } } #[cfg(test)] mod tests { use super::{EventId, OwnedEventId}; use crate::IdParseError; #[test] fn valid_original_event_id() { assert_eq!( <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId."), "$39hvsi03hlne:example.com" ); } #[test] fn valid_base64_event_id() { assert_eq!( <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") .expect("Failed to create EventId."), "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" ); } #[test] fn valid_url_safe_base64_event_id() { assert_eq!( <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") .expect("Failed to create EventId."), "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" ); } #[cfg(feature = "rand")] #[test] fn generate_random_valid_event_id() { use crate::server_name; let event_id = EventId::new(server_name!("example.com")); let id_str = event_id.as_str(); assert!(id_str.starts_with('$')); assert_eq!(id_str.len(), 31); } #[test] fn serialize_valid_original_event_id() { assert_eq!( serde_json::to_string( <&EventId>::try_from("$39hvsi03hlne:example.com") .expect("Failed to create EventId.") ) .expect("Failed to convert EventId to JSON."), r#""$39hvsi03hlne:example.com""# ); } #[test] fn serialize_valid_base64_event_id() { assert_eq!( serde_json::to_string( <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") .expect("Failed to create EventId.") ) .expect("Failed to convert EventId to JSON."), r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""# ); } #[test] fn serialize_valid_url_safe_base64_event_id() { assert_eq!( serde_json::to_string( <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") .expect("Failed to create EventId.") ) .expect("Failed to convert EventId to JSON."), r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""# ); } #[test] fn deserialize_valid_original_event_id() { assert_eq!( serde_json::from_str::(r#""$39hvsi03hlne:example.com""#) .expect("Failed to convert JSON to EventId"), <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") ); } #[test] fn deserialize_valid_base64_event_id() { assert_eq!( serde_json::from_str::( r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""# ) .expect("Failed to convert JSON to EventId"), <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") .expect("Failed to create EventId.") ); } #[test] fn deserialize_valid_url_safe_base64_event_id() { assert_eq!( serde_json::from_str::( r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""# ) .expect("Failed to convert JSON to EventId"), <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") .expect("Failed to create EventId.") ); } #[test] fn valid_original_event_id_with_explicit_standard_port() { assert_eq!( <&EventId>::try_from("$39hvsi03hlne:example.com:443") .expect("Failed to create EventId."), "$39hvsi03hlne:example.com:443" ); } #[test] fn valid_original_event_id_with_non_standard_port() { assert_eq!( <&EventId>::try_from("$39hvsi03hlne:example.com:5000") .expect("Failed to create EventId."), "$39hvsi03hlne:example.com:5000" ); } #[test] fn missing_original_event_id_sigil() { assert_eq!( <&EventId>::try_from("39hvsi03hlne:example.com").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn missing_base64_event_id_sigil() { assert_eq!( <&EventId>::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn missing_url_safe_base64_event_id_sigil() { assert_eq!( <&EventId>::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn invalid_event_id_host() { assert_eq!( <&EventId>::try_from("$39hvsi03hlne:/").unwrap_err(), IdParseError::InvalidServerName ); } #[test] fn invalid_event_id_port() { assert_eq!( <&EventId>::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(), IdParseError::InvalidServerName ); } } ruma-common-0.10.5/src/identifiers/key_id.rs000064400000000000000000000054571046102023000170620ustar 00000000000000use std::{ cmp::Ordering, hash::{Hash, Hasher}, marker::PhantomData, str::FromStr, }; use ruma_macros::IdZst; use super::{crypto_algorithms::SigningKeyAlgorithm, DeviceId, KeyName}; /// A key algorithm and key name delimited by a colon. #[repr(transparent)] #[derive(IdZst)] #[ruma_id(validate = ruma_identifiers_validation::key_id::validate)] pub struct KeyId(PhantomData<(A, K)>, str); impl KeyId { /// Creates a new `KeyId` from an algorithm and key name. pub fn from_parts(algorithm: A, key_name: &K) -> OwnedKeyId where A: AsRef, K: AsRef, { let algorithm = algorithm.as_ref(); let key_name = key_name.as_ref(); let mut res = String::with_capacity(algorithm.len() + 1 + key_name.len()); res.push_str(algorithm); res.push(':'); res.push_str(key_name); Self::from_borrowed(&res).to_owned() } /// Returns key algorithm of the key ID. pub fn algorithm(&self) -> A where A: FromStr, { A::from_str(&self.as_str()[..self.colon_idx()]).unwrap_or_else(|_| unreachable!()) } /// Returns the key name of the key ID. pub fn key_name<'a>(&'a self) -> &'a K where &'a K: From<&'a str>, { self.as_str()[self.colon_idx() + 1..].into() } fn colon_idx(&self) -> usize { self.as_str().find(':').unwrap() } } /// Algorithm + key name for signing keys. pub type SigningKeyId = KeyId; /// Algorithm + key name for signing keys. pub type OwnedSigningKeyId = OwnedKeyId; /// Algorithm + key name for homeserver signing keys. pub type ServerSigningKeyId = SigningKeyId; /// Algorithm + key name for homeserver signing keys. pub type OwnedServerSigningKeyId = OwnedSigningKeyId; /// Algorithm + key name for device keys. pub type DeviceSigningKeyId = SigningKeyId; /// Algorithm + key name for device keys. pub type OwnedDeviceSigningKeyId = OwnedSigningKeyId; // The following impls are usually derived using the std macros. // They are implemented manually here to avoid unnecessary bounds. impl PartialEq for KeyId { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } impl Eq for KeyId {} impl PartialOrd for KeyId { fn partial_cmp(&self, other: &Self) -> Option { PartialOrd::partial_cmp(self.as_str(), other.as_str()) } } impl Ord for KeyId { fn cmp(&self, other: &Self) -> Ordering { Ord::cmp(self.as_str(), other.as_str()) } } impl Hash for KeyId { fn hash(&self, state: &mut H) { self.as_str().hash(state); } } ruma-common-0.10.5/src/identifiers/key_name.rs000064400000000000000000000004451046102023000173760ustar 00000000000000use ruma_macros::IdZst; /// A Matrix key identifier. /// /// Key identifiers in Matrix are opaque character sequences of `[a-zA-Z_]`. This type is /// provided simply for its semantic value. #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] pub struct KeyName(str); ruma-common-0.10.5/src/identifiers/matrix_uri.rs000064400000000000000000001143501046102023000177720ustar 00000000000000//! Matrix URIs. use std::{fmt, str::FromStr}; use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}; use ruma_identifiers_validation::{ error::{MatrixIdError, MatrixToError, MatrixUriError}, Error, }; use url::Url; use super::{ EventId, OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId, }; use crate::{PrivOwnedStr, ServerName}; const MATRIX_TO_BASE_URL: &str = "https://matrix.to/#/"; const MATRIX_SCHEME: &str = "matrix"; // Controls + Space + non-path characters from RFC 3986. In practice only the // non-path characters will be encountered most likely, but better be safe. // https://datatracker.ietf.org/doc/html/rfc3986/#page-23 const NON_PATH: &AsciiSet = &CONTROLS.add(b'/').add(b'?').add(b'#').add(b'[').add(b']'); // Controls + Space + reserved characters from RFC 3986. In practice only the // reserved characters will be encountered most likely, but better be safe. // https://datatracker.ietf.org/doc/html/rfc3986/#page-13 const RESERVED: &AsciiSet = &CONTROLS .add(b':') .add(b'/') .add(b'?') .add(b'#') .add(b'[') .add(b']') .add(b'@') .add(b'!') .add(b'$') .add(b'&') .add(b'\'') .add(b'(') .add(b')') .add(b'*') .add(b'+') .add(b',') .add(b';') .add(b'='); /// All Matrix Identifiers that can be represented as a Matrix URI. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum MatrixId { /// A room ID. Room(OwnedRoomId), /// A room alias. RoomAlias(OwnedRoomAliasId), /// A user ID. User(OwnedUserId), /// An event ID. Event(OwnedRoomOrAliasId, OwnedEventId), } impl MatrixId { /// Try parsing a `&str` with sigils into a `MatrixId`. /// /// The identifiers are expected to start with a sigil and to be percent /// encoded. Slashes at the beginning and the end are stripped. /// /// For events, the room ID or alias and the event ID should be separated by /// a slash and they can be in any order. pub(crate) fn parse_with_sigil(s: &str) -> Result { let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s }; let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s }; if s.is_empty() { return Err(MatrixIdError::NoIdentifier.into()); } if s.matches('/').count() > 1 { return Err(MatrixIdError::TooManyIdentifiers.into()); } if let Some((first_raw, second_raw)) = s.split_once('/') { let first = percent_decode_str(first_raw).decode_utf8()?; let second = percent_decode_str(second_raw).decode_utf8()?; match first.as_bytes()[0] { b'!' | b'#' if second.as_bytes()[0] == b'$' => { let room_id = <&RoomOrAliasId>::try_from(first.as_ref())?; let event_id = <&EventId>::try_from(second.as_ref())?; Ok((room_id, event_id).into()) } b'$' if matches!(second.as_bytes()[0], b'!' | b'#') => { let room_id = <&RoomOrAliasId>::try_from(second.as_ref())?; let event_id = <&EventId>::try_from(first.as_ref())?; Ok((room_id, event_id).into()) } _ => Err(MatrixIdError::UnknownIdentifierPair.into()), } } else { let id = percent_decode_str(s).decode_utf8()?; match id.as_bytes()[0] { b'@' => Ok(<&UserId>::try_from(id.as_ref())?.into()), b'!' => Ok(<&RoomId>::try_from(id.as_ref())?.into()), b'#' => Ok(<&RoomAliasId>::try_from(id.as_ref())?.into()), b'$' => Err(MatrixIdError::MissingRoom.into()), _ => Err(MatrixIdError::UnknownIdentifier.into()), } } } /// Try parsing a `&str` with types into a `MatrixId`. /// /// The identifiers are expected to be in the format /// `type/identifier_without_sigil` and the identifier part is expected to /// be percent encoded. Slashes at the beginning and the end are stripped. /// /// For events, the room ID or alias and the event ID should be separated by /// a slash and they can be in any order. pub(crate) fn parse_with_type(s: &str) -> Result { let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s }; let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s }; if s.is_empty() { return Err(MatrixIdError::NoIdentifier.into()); } if ![1, 3].contains(&s.matches('/').count()) { return Err(MatrixIdError::InvalidPartsNumber.into()); } let mut id = String::new(); let mut split = s.split('/'); while let (Some(type_), Some(id_without_sigil)) = (split.next(), split.next()) { let sigil = match type_ { "u" | "user" => '@', "r" | "room" => '#', "e" | "event" => '$', "roomid" => '!', _ => return Err(MatrixIdError::UnknownType.into()), }; id = format!("{id}/{sigil}{id_without_sigil}"); } Self::parse_with_sigil(&id) } /// Construct a string with sigils from `self`. /// /// The identifiers will start with a sigil and be percent encoded. /// /// For events, the room ID or alias and the event ID will be separated by /// a slash. pub(crate) fn to_string_with_sigil(&self) -> String { match self { Self::Room(room_id) => percent_encode(room_id.as_bytes(), RESERVED).to_string(), Self::RoomAlias(room_alias) => { percent_encode(room_alias.as_bytes(), RESERVED).to_string() } Self::User(user_id) => percent_encode(user_id.as_bytes(), RESERVED).to_string(), Self::Event(room_id, event_id) => format!( "{}/{}", percent_encode(room_id.as_bytes(), RESERVED), percent_encode(event_id.as_bytes(), RESERVED), ), } } /// Construct a string with types from `self`. /// /// The identifiers will be in the format `type/identifier_without_sigil` /// and the identifier part will be percent encoded. /// /// For events, the room ID or alias and the event ID will be separated by /// a slash. pub(crate) fn to_string_with_type(&self) -> String { match self { Self::Room(room_id) => { format!("roomid/{}", percent_encode(&room_id.as_bytes()[1..], NON_PATH)) } Self::RoomAlias(room_alias) => { format!("r/{}", percent_encode(&room_alias.as_bytes()[1..], NON_PATH)) } Self::User(user_id) => { format!("u/{}", percent_encode(&user_id.as_bytes()[1..], NON_PATH)) } Self::Event(room_id, event_id) => { let room_type = if room_id.is_room_id() { "roomid" } else { "r" }; format!( "{}/{}/e/{}", room_type, percent_encode(&room_id.as_bytes()[1..], NON_PATH), percent_encode(&event_id.as_bytes()[1..], NON_PATH), ) } } } } impl From for MatrixId { fn from(room_id: OwnedRoomId) -> Self { Self::Room(room_id) } } impl From<&RoomId> for MatrixId { fn from(room_id: &RoomId) -> Self { room_id.to_owned().into() } } impl From for MatrixId { fn from(room_alias: OwnedRoomAliasId) -> Self { Self::RoomAlias(room_alias) } } impl From<&RoomAliasId> for MatrixId { fn from(room_alias: &RoomAliasId) -> Self { room_alias.to_owned().into() } } impl From for MatrixId { fn from(user_id: OwnedUserId) -> Self { Self::User(user_id) } } impl From<&UserId> for MatrixId { fn from(user_id: &UserId) -> Self { user_id.to_owned().into() } } impl From<(OwnedRoomOrAliasId, OwnedEventId)> for MatrixId { fn from(ids: (OwnedRoomOrAliasId, OwnedEventId)) -> Self { Self::Event(ids.0, ids.1) } } impl From<(&RoomOrAliasId, &EventId)> for MatrixId { fn from(ids: (&RoomOrAliasId, &EventId)) -> Self { (ids.0.to_owned(), ids.1.to_owned()).into() } } impl From<(OwnedRoomId, OwnedEventId)> for MatrixId { fn from(ids: (OwnedRoomId, OwnedEventId)) -> Self { Self::Event(ids.0.into(), ids.1) } } impl From<(&RoomId, &EventId)> for MatrixId { fn from(ids: (&RoomId, &EventId)) -> Self { (ids.0.to_owned(), ids.1.to_owned()).into() } } impl From<(OwnedRoomAliasId, OwnedEventId)> for MatrixId { fn from(ids: (OwnedRoomAliasId, OwnedEventId)) -> Self { Self::Event(ids.0.into(), ids.1) } } impl From<(&RoomAliasId, &EventId)> for MatrixId { fn from(ids: (&RoomAliasId, &EventId)) -> Self { (ids.0.to_owned(), ids.1.to_owned()).into() } } /// The [`matrix.to` URI] representation of a user, room or event. /// /// Get the URI through its `Display` implementation (i.e. by interpolating it /// in a formatting macro or via `.to_string()`). /// /// [`matrix.to` URI]: https://spec.matrix.org/v1.2/appendices/#matrixto-navigation #[derive(Debug, PartialEq, Eq)] pub struct MatrixToUri { id: MatrixId, via: Vec, } impl MatrixToUri { pub(crate) fn new(id: MatrixId, via: Vec) -> Self { Self { id, via } } /// The identifier represented by this `matrix.to` URI. pub fn id(&self) -> &MatrixId { &self.id } /// Matrix servers usable to route a `RoomId`. pub fn via(&self) -> &[OwnedServerName] { &self.via } /// Try parsing a `&str` into a `MatrixToUri`. pub fn parse(s: &str) -> Result { // We do not rely on parsing with `url::Url` because the meaningful part // of the URI is in its fragment part. // // Even if the fragment part looks like parts of a URI, non-url-encoded // room aliases (starting with `#`) could be detected as fragments, // messing up the URI parsing. // // A matrix.to URI looks like this: https://matrix.to/#/{MatrixId}?{query}; // where the MatrixId should be percent-encoded, but might not, and the query // should also be percent-encoded. let s = s.strip_prefix(MATRIX_TO_BASE_URL).ok_or(MatrixToError::WrongBaseUrl)?; let s = s.strip_suffix('/').unwrap_or(s); // Separate the identifiers and the query. let mut parts = s.split('?'); let ids_part = parts.next().expect("a split iterator yields at least one value"); let id = MatrixId::parse_with_sigil(ids_part)?; // Parse the query for routing arguments. let via = parts .next() .map(|query| { // `form_urlencoded` takes care of percent-decoding the query. let query_parts = form_urlencoded::parse(query.as_bytes()); query_parts .map(|(key, value)| { (key == "via") .then(|| ServerName::parse(&value)) .unwrap_or_else(|| Err(MatrixToError::UnknownArgument.into())) }) .collect::, _>>() }) .transpose()? .unwrap_or_default(); // That would mean there are two `?` in the URL which is not valid. if parts.next().is_some() { return Err(MatrixToError::InvalidUrl.into()); } Ok(Self { id, via }) } } impl fmt::Display for MatrixToUri { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(MATRIX_TO_BASE_URL)?; write!(f, "{}", self.id().to_string_with_sigil())?; let mut first = true; for server_name in &self.via { f.write_str(if first { "?via=" } else { "&via=" })?; f.write_str(server_name.as_str())?; first = false; } Ok(()) } } impl TryFrom<&str> for MatrixToUri { type Error = Error; fn try_from(s: &str) -> Result { Self::parse(s) } } impl FromStr for MatrixToUri { type Err = Error; fn from_str(s: &str) -> Result { Self::parse(s) } } /// The intent of a Matrix URI. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum UriAction { /// Join the room referenced by the URI. /// /// The client should prompt for confirmation prior to joining the room, if /// the user isn’t already part of the room. Join, /// Start a direct chat with the user referenced by the URI. /// /// Clients supporting a form of Canonical DMs should reuse existing DMs /// instead of creating new ones if available. The client should prompt for /// confirmation prior to creating the DM, if the user isn’t being /// redirected to an existing canonical DM. Chat, #[doc(hidden)] _Custom(PrivOwnedStr), } impl UriAction { /// Creates a string slice from this `UriAction`. pub fn as_str(&self) -> &str { self.as_ref() } fn from(s: T) -> Self where T: AsRef + Into>, { match s.as_ref() { "join" => UriAction::Join, "chat" => UriAction::Chat, _ => UriAction::_Custom(PrivOwnedStr(s.into())), } } } impl AsRef for UriAction { fn as_ref(&self) -> &str { match self { UriAction::Join => "join", UriAction::Chat => "chat", UriAction::_Custom(s) => s.0.as_ref(), } } } impl fmt::Display for UriAction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_ref())?; Ok(()) } } impl From<&str> for UriAction { fn from(s: &str) -> Self { Self::from(s) } } impl From for UriAction { fn from(s: String) -> Self { Self::from(s) } } impl From> for UriAction { fn from(s: Box) -> Self { Self::from(s) } } /// The [`matrix:` URI] representation of a user, room or event. /// /// Get the URI through its `Display` implementation (i.e. by interpolating it /// in a formatting macro or via `.to_string()`). /// /// [`matrix:` URI]: https://spec.matrix.org/v1.2/appendices/#matrix-uri-scheme #[derive(Debug, PartialEq, Eq)] pub struct MatrixUri { id: MatrixId, via: Vec, action: Option, } impl MatrixUri { pub(crate) fn new(id: MatrixId, via: Vec, action: Option) -> Self { Self { id, via, action } } /// The identifier represented by this `matrix:` URI. pub fn id(&self) -> &MatrixId { &self.id } /// Matrix servers usable to route a `RoomId`. pub fn via(&self) -> &[OwnedServerName] { &self.via } /// The intent of this URI. pub fn action(&self) -> Option<&UriAction> { self.action.as_ref() } /// Try parsing a `&str` into a `MatrixUri`. pub fn parse(s: &str) -> Result { let url = Url::parse(s).map_err(|_| MatrixToError::InvalidUrl)?; if url.scheme() != MATRIX_SCHEME { return Err(MatrixUriError::WrongScheme.into()); } let id = MatrixId::parse_with_type(url.path())?; let mut via = vec![]; let mut action = None; for (key, value) in url.query_pairs() { if key.as_ref() == "via" { via.push(value.parse()?); } else if key.as_ref() == "action" { if action.is_some() { return Err(MatrixUriError::TooManyActions.into()); }; action = Some(value.as_ref().into()); } else { return Err(MatrixUriError::UnknownQueryItem.into()); } } Ok(Self { id, via, action }) } } impl fmt::Display for MatrixUri { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{MATRIX_SCHEME}:{}", self.id().to_string_with_type())?; let mut first = true; for server_name in &self.via { f.write_str(if first { "?via=" } else { "&via=" })?; f.write_str(server_name.as_str())?; first = false; } if let Some(action) = self.action() { f.write_str(if first { "?action=" } else { "&action=" })?; f.write_str(action.as_str())?; } Ok(()) } } impl TryFrom<&str> for MatrixUri { type Error = Error; fn try_from(s: &str) -> Result { Self::parse(s) } } impl FromStr for MatrixUri { type Err = Error; fn from_str(s: &str) -> Result { Self::parse(s) } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use ruma_identifiers_validation::{ error::{MatrixIdError, MatrixToError, MatrixUriError}, Error, }; use super::{MatrixId, MatrixToUri, MatrixUri}; use crate::{ event_id, matrix_uri::UriAction, room_alias_id, room_id, server_name, user_id, RoomOrAliasId, }; #[test] fn display_matrixtouri() { assert_eq!( user_id!("@jplatte:notareal.hs").matrix_to_uri().to_string(), "https://matrix.to/#/%40jplatte%3Anotareal.hs" ); assert_eq!( room_alias_id!("#ruma:notareal.hs").matrix_to_uri().to_string(), "https://matrix.to/#/%23ruma%3Anotareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs").matrix_to_uri().to_string(), "https://matrix.to/#/%21ruma%3Anotareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_to_uri_via(vec![server_name!("notareal.hs")]) .to_string(), "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs" ); assert_eq!( room_alias_id!("#ruma:notareal.hs") .matrix_to_event_uri(event_id!("$event:notareal.hs")) .to_string(), "https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_to_event_uri(event_id!("$event:notareal.hs")) .to_string(), "https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_to_event_uri_via( event_id!("$event:notareal.hs"), vec![server_name!("notareal.hs")] ) .to_string(), "https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs?via=notareal.hs" ); } #[test] fn parse_valid_matrixid_with_sigil() { assert_eq!( MatrixId::parse_with_sigil("@user:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::Room(room_id!("!roomid:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_sigil("#roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs/$event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_sigil("#roomalias:imaginary.hs/$event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); // Invert the order of the event and the room. assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs/!roomid:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs/#roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); // Starting with a slash assert_eq!( MatrixId::parse_with_sigil("/@user:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); // Ending with a slash assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs/") .expect("Failed to create MatrixId."), MatrixId::Room(room_id!("!roomid:imaginary.hs").into()) ); // Starting and ending with a slash assert_eq!( MatrixId::parse_with_sigil("/#roomalias:imaginary.hs/") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); } #[test] fn parse_matrixid_no_identifier() { assert_eq!(MatrixId::parse_with_sigil("").unwrap_err(), MatrixIdError::NoIdentifier.into()); assert_eq!( MatrixId::parse_with_sigil("/").unwrap_err(), MatrixIdError::NoIdentifier.into() ); } #[test] fn parse_matrixid_too_many_identifiers() { assert_eq!( MatrixId::parse_with_sigil( "@user:imaginary.hs/#room:imaginary.hs/$event1:imaginary.hs" ) .unwrap_err(), MatrixIdError::TooManyIdentifiers.into() ); } #[test] fn parse_matrixid_unknown_identifier_pair() { assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs/@user:imaginary.hs").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); assert_eq!( MatrixId::parse_with_sigil("#roomalias:imaginary.hs/notanidentifier").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs/$otherevent:imaginary.hs").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); assert_eq!( MatrixId::parse_with_sigil("notanidentifier/neitheristhis").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); } #[test] fn parse_matrixid_missing_room() { assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs").unwrap_err(), MatrixIdError::MissingRoom.into() ); } #[test] fn parse_matrixid_unknown_identifier() { assert_eq!( MatrixId::parse_with_sigil("event:imaginary.hs").unwrap_err(), MatrixIdError::UnknownIdentifier.into() ); assert_eq!( MatrixId::parse_with_sigil("notanidentifier").unwrap_err(), MatrixIdError::UnknownIdentifier.into() ); } #[test] fn parse_matrixtouri_valid_uris() { let matrix_to = MatrixToUri::parse("https://matrix.to/#/%40jplatte%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &user_id!("@jplatte:notareal.hs").into()); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &room_alias_id!("#ruma:notareal.hs").into()); let matrix_to = MatrixToUri::parse( "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&via=anotherunreal.hs", ) .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &room_id!("!ruma:notareal.hs").into()); assert_eq!( matrix_to.via(), &[server_name!("notareal.hs").to_owned(), server_name!("anotherunreal.hs").to_owned(),] ); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_to.id(), &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_to.id(), &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); assert_eq!(matrix_to.via().len(), 0); } #[test] fn parse_matrixtouri_valid_uris_not_urlencoded() { let matrix_to = MatrixToUri::parse("https://matrix.to/#/@jplatte:notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &user_id!("@jplatte:notareal.hs").into()); let matrix_to = MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &room_alias_id!("#ruma:notareal.hs").into()); let matrix_to = MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs?via=notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &room_id!("!ruma:notareal.hs").into()); assert_eq!(matrix_to.via(), &[server_name!("notareal.hs").to_owned()]); let matrix_to = MatrixToUri::parse("https://matrix.to/#/#ruma:notareal.hs/$event:notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_to.id(), &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); let matrix_to = MatrixToUri::parse("https://matrix.to/#/!ruma:notareal.hs/$event:notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_to.id(), &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); assert_eq!(matrix_to.via().len(), 0); } #[test] fn parse_matrixtouri_wrong_base_url() { assert_eq!(MatrixToUri::parse("").unwrap_err(), MatrixToError::WrongBaseUrl.into()); assert_eq!( MatrixToUri::parse("https://notreal.to/#/").unwrap_err(), MatrixToError::WrongBaseUrl.into() ); } #[test] fn parse_matrixtouri_wrong_identifier() { assert_matches!( MatrixToUri::parse("https://matrix.to/#/notanidentifier").unwrap_err(), Error::InvalidMatrixId(_) ); assert_matches!( MatrixToUri::parse("https://matrix.to/#/").unwrap_err(), Error::InvalidMatrixId(_) ); assert_matches!( MatrixToUri::parse( "https://matrix.to/#/%40jplatte%3Anotareal.hs/%24event%3Anotareal.hs" ) .unwrap_err(), Error::InvalidMatrixId(_) ); } #[test] fn parse_matrixtouri_unknown_arguments() { assert_eq!( MatrixToUri::parse( "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&custom=data" ) .unwrap_err(), MatrixToError::UnknownArgument.into() ); } #[test] fn display_matrixuri() { assert_eq!( user_id!("@jplatte:notareal.hs").matrix_uri(false).to_string(), "matrix:u/jplatte:notareal.hs" ); assert_eq!( user_id!("@jplatte:notareal.hs").matrix_uri(true).to_string(), "matrix:u/jplatte:notareal.hs?action=chat" ); assert_eq!( room_alias_id!("#ruma:notareal.hs").matrix_uri(false).to_string(), "matrix:r/ruma:notareal.hs" ); assert_eq!( room_alias_id!("#ruma:notareal.hs").matrix_uri(true).to_string(), "matrix:r/ruma:notareal.hs?action=join" ); assert_eq!( room_id!("!ruma:notareal.hs").matrix_uri(false).to_string(), "matrix:roomid/ruma:notareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_uri_via(vec![server_name!("notareal.hs")], false) .to_string(), "matrix:roomid/ruma:notareal.hs?via=notareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_uri_via( vec![server_name!("notareal.hs"), server_name!("anotherunreal.hs")], true ) .to_string(), "matrix:roomid/ruma:notareal.hs?via=notareal.hs&via=anotherunreal.hs&action=join" ); assert_eq!( room_alias_id!("#ruma:notareal.hs") .matrix_event_uri(event_id!("$event:notareal.hs")) .to_string(), "matrix:r/ruma:notareal.hs/e/event:notareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_event_uri(event_id!("$event:notareal.hs")) .to_string(), "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_event_uri_via( event_id!("$event:notareal.hs"), vec![server_name!("notareal.hs")] ) .to_string(), "matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs" ); } #[test] fn parse_valid_matrixid_with_type() { assert_eq!( MatrixId::parse_with_type("u/user:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_type("user/user:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_type("roomid/roomid:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Room(room_id!("!roomid:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_type("r/roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_type("room/roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_type("roomid/roomid:imaginary.hs/e/event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_type("r/roomalias:imaginary.hs/e/event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_type("room/roomalias:imaginary.hs/event/event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); // Invert the order of the event and the room. assert_eq!( MatrixId::parse_with_type("e/event:imaginary.hs/roomid/roomid:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_type("e/event:imaginary.hs/r/roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); // Starting with a slash assert_eq!( MatrixId::parse_with_type("/u/user:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); // Ending with a slash assert_eq!( MatrixId::parse_with_type("roomid/roomid:imaginary.hs/") .expect("Failed to create MatrixId."), MatrixId::Room(room_id!("!roomid:imaginary.hs").into()) ); // Starting and ending with a slash assert_eq!( MatrixId::parse_with_type("/r/roomalias:imaginary.hs/") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); } #[test] fn parse_matrixid_type_no_identifier() { assert_eq!(MatrixId::parse_with_type("").unwrap_err(), MatrixIdError::NoIdentifier.into()); assert_eq!(MatrixId::parse_with_type("/").unwrap_err(), MatrixIdError::NoIdentifier.into()); } #[test] fn parse_matrixid_invalid_parts_number() { assert_eq!( MatrixId::parse_with_type("u/user:imaginary.hs/r/room:imaginary.hs/e").unwrap_err(), MatrixIdError::InvalidPartsNumber.into() ); } #[test] fn parse_matrixid_unknown_type() { assert_eq!( MatrixId::parse_with_type("notatype/fake:notareal.hs").unwrap_err(), MatrixIdError::UnknownType.into() ); } #[test] fn parse_matrixuri_valid_uris() { let matrix_uri = MatrixUri::parse("matrix:u/jplatte:notareal.hs").expect("Failed to create MatrixUri."); assert_eq!(matrix_uri.id(), &user_id!("@jplatte:notareal.hs").into()); assert_eq!(matrix_uri.action(), None); let matrix_uri = MatrixUri::parse("matrix:u/jplatte:notareal.hs?action=chat") .expect("Failed to create MatrixUri."); assert_eq!(matrix_uri.id(), &user_id!("@jplatte:notareal.hs").into()); assert_eq!(matrix_uri.action(), Some(&UriAction::Chat)); let matrix_uri = MatrixUri::parse("matrix:r/ruma:notareal.hs").expect("Failed to create MatrixToUri."); assert_eq!(matrix_uri.id(), &room_alias_id!("#ruma:notareal.hs").into()); let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs?via=notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_uri.id(), &room_id!("!ruma:notareal.hs").into()); assert_eq!(matrix_uri.via(), &[server_name!("notareal.hs").to_owned()]); assert_eq!(matrix_uri.action(), None); let matrix_uri = MatrixUri::parse("matrix:r/ruma:notareal.hs/e/event:notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_uri.id(), &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_uri.id(), &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); assert_eq!(matrix_uri.via().len(), 0); assert_eq!(matrix_uri.action(), None); let matrix_uri = MatrixUri::parse("matrix:roomid/ruma:notareal.hs/e/event:notareal.hs?via=notareal.hs&action=join&via=anotherinexistant.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_uri.id(), &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); assert_eq!( matrix_uri.via(), &vec![ server_name!("notareal.hs").to_owned(), server_name!("anotherinexistant.hs").to_owned() ] ); assert_eq!(matrix_uri.action(), Some(&UriAction::Join)); } #[test] fn parse_matrixuri_invalid_uri() { assert_eq!( MatrixUri::parse("").unwrap_err(), Error::InvalidMatrixToUri(MatrixToError::InvalidUrl) ); } #[test] fn parse_matrixuri_wrong_scheme() { assert_eq!( MatrixUri::parse("unknown:u/user:notareal.hs").unwrap_err(), MatrixUriError::WrongScheme.into() ); } #[test] fn parse_matrixuri_too_many_actions() { assert_eq!( MatrixUri::parse("matrix:u/user:notareal.hs?action=chat&action=join").unwrap_err(), MatrixUriError::TooManyActions.into() ); } #[test] fn parse_matrixuri_unknown_query_item() { assert_eq!( MatrixUri::parse("matrix:roomid/roomid:notareal.hs?via=notareal.hs&fake=data") .unwrap_err(), MatrixUriError::UnknownQueryItem.into() ); } #[test] fn parse_matrixuri_wrong_identifier() { assert_matches!( MatrixUri::parse("matrix:notanidentifier").unwrap_err(), Error::InvalidMatrixId(_) ); assert_matches!(MatrixUri::parse("matrix:").unwrap_err(), Error::InvalidMatrixId(_)); assert_matches!( MatrixUri::parse("matrix:u/jplatte:notareal.hs/e/event:notareal.hs").unwrap_err(), Error::InvalidMatrixId(_) ); } } ruma-common-0.10.5/src/identifiers/mxc_uri.rs000064400000000000000000000063241046102023000172560ustar 00000000000000//! A URI that should be a Matrix-spec compliant [MXC URI]. //! //! [MXC URI]: https://spec.matrix.org/v1.2/client-server-api/#matrix-content-mxc-uris use std::num::NonZeroU8; use ruma_identifiers_validation::{error::MxcUriError, mxc_uri::validate}; use ruma_macros::IdZst; use super::ServerName; type Result = std::result::Result; /// A URI that should be a Matrix-spec compliant [MXC URI]. /// /// [MXC URI]: https://spec.matrix.org/v1.2/client-server-api/#matrix-content-mxc-uris #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] pub struct MxcUri(str); impl MxcUri { /// If this is a valid MXC URI, returns the media ID. pub fn media_id(&self) -> Result<&str> { self.parts().map(|(_, s)| s) } /// If this is a valid MXC URI, returns the server name. pub fn server_name(&self) -> Result<&ServerName> { self.parts().map(|(s, _)| s) } /// If this is a valid MXC URI, returns a `(server_name, media_id)` tuple, else it returns the /// error. pub fn parts(&self) -> Result<(&ServerName, &str)> { self.extract_slash_idx().map(|idx| { ( ServerName::from_borrowed(&self.as_str()[6..idx.get() as usize]), &self.as_str()[idx.get() as usize + 1..], ) }) } /// Validates the URI and returns an error if it failed. pub fn validate(&self) -> Result<()> { self.extract_slash_idx().map(|_| ()) } /// Convenience method for `.validate().is_ok()`. #[inline(always)] pub fn is_valid(&self) -> bool { self.validate().is_ok() } // convenience method for calling validate(self) #[inline(always)] fn extract_slash_idx(&self) -> Result { validate(self.as_str()) } } #[cfg(test)] mod tests { use ruma_identifiers_validation::error::MxcUriError; use super::{MxcUri, OwnedMxcUri}; #[test] fn parse_mxc_uri() { let mxc = Box::::from("mxc://127.0.0.1/asd32asdfasdsd"); assert!(mxc.is_valid()); assert_eq!( mxc.parts(), Ok(("127.0.0.1".try_into().expect("Failed to create ServerName"), "asd32asdfasdsd")) ); } #[test] fn parse_mxc_uri_without_media_id() { let mxc = Box::::from("mxc://127.0.0.1"); assert!(!mxc.is_valid()); assert_eq!(mxc.parts(), Err(MxcUriError::MissingSlash)); } #[test] fn parse_mxc_uri_without_protocol() { assert!(!Box::::from("127.0.0.1/asd32asdfasdsd").is_valid()); } #[test] fn serialize_mxc_uri() { assert_eq!( serde_json::to_string(&Box::::from("mxc://server/1234id")) .expect("Failed to convert MxcUri to JSON."), r#""mxc://server/1234id""# ); } #[test] fn deserialize_mxc_uri() { let mxc = serde_json::from_str::(r#""mxc://server/1234id""#) .expect("Failed to convert JSON to MxcUri"); assert_eq!(mxc.as_str(), "mxc://server/1234id"); assert!(mxc.is_valid()); assert_eq!( mxc.parts(), Ok(("server".try_into().expect("Failed to create ServerName"), "1234id")) ); } } ruma-common-0.10.5/src/identifiers/room_alias_id.rs000064400000000000000000000116711046102023000204120ustar 00000000000000//! Matrix room alias identifiers. use ruma_macros::IdZst; use super::{matrix_uri::UriAction, server_name::ServerName, MatrixToUri, MatrixUri, OwnedEventId}; /// A Matrix [room alias ID]. /// /// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as /// needed. /// /// ``` /// # use ruma_common::RoomAliasId; /// assert_eq!(<&RoomAliasId>::try_from("#ruma:example.com").unwrap(), "#ruma:example.com"); /// ``` /// /// [room alias ID]: https://spec.matrix.org/v1.2/appendices/#room-aliases #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::room_alias_id::validate)] pub struct RoomAliasId(str); impl RoomAliasId { /// Returns the room's alias. pub fn alias(&self) -> &str { &self.as_str()[1..self.colon_idx()] } /// Returns the server name of the room alias ID. pub fn server_name(&self) -> &ServerName { ServerName::from_borrowed(&self.as_str()[self.colon_idx() + 1..]) } /// Create a `matrix.to` URI for this room alias ID. pub fn matrix_to_uri(&self) -> MatrixToUri { MatrixToUri::new(self.into(), Vec::new()) } /// Create a `matrix.to` URI for an event scoped under this room alias ID. pub fn matrix_to_event_uri(&self, ev_id: impl Into) -> MatrixToUri { MatrixToUri::new((self.to_owned(), ev_id.into()).into(), Vec::new()) } /// Create a `matrix:` URI for this room alias ID. /// /// If `join` is `true`, a click on the URI should join the room. pub fn matrix_uri(&self, join: bool) -> MatrixUri { MatrixUri::new(self.into(), Vec::new(), Some(UriAction::Join).filter(|_| join)) } /// Create a `matrix:` URI for an event scoped under this room alias ID. pub fn matrix_event_uri(&self, ev_id: impl Into) -> MatrixUri { MatrixUri::new((self.to_owned(), ev_id.into()).into(), Vec::new(), None) } fn colon_idx(&self) -> usize { self.as_str().find(':').unwrap() } } #[cfg(test)] mod tests { use super::{OwnedRoomAliasId, RoomAliasId}; use crate::IdParseError; #[test] fn valid_room_alias_id() { assert_eq!( <&RoomAliasId>::try_from("#ruma:example.com").expect("Failed to create RoomAliasId."), "#ruma:example.com" ); } #[test] fn empty_localpart() { assert_eq!( <&RoomAliasId>::try_from("#:myhomeserver.io").expect("Failed to create RoomAliasId."), "#:myhomeserver.io" ); } #[test] fn serialize_valid_room_alias_id() { assert_eq!( serde_json::to_string( <&RoomAliasId>::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") ) .expect("Failed to convert RoomAliasId to JSON."), r##""#ruma:example.com""## ); } #[test] fn deserialize_valid_room_alias_id() { assert_eq!( serde_json::from_str::(r##""#ruma:example.com""##) .expect("Failed to convert JSON to RoomAliasId"), <&RoomAliasId>::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") ); } #[test] fn valid_room_alias_id_with_explicit_standard_port() { assert_eq!( <&RoomAliasId>::try_from("#ruma:example.com:443") .expect("Failed to create RoomAliasId."), "#ruma:example.com:443" ); } #[test] fn valid_room_alias_id_with_non_standard_port() { assert_eq!( <&RoomAliasId>::try_from("#ruma:example.com:5000") .expect("Failed to create RoomAliasId."), "#ruma:example.com:5000" ); } #[test] fn valid_room_alias_id_unicode() { assert_eq!( <&RoomAliasId>::try_from("#老虎£я:example.com") .expect("Failed to create RoomAliasId."), "#老虎£я:example.com" ); } #[test] fn missing_room_alias_id_sigil() { assert_eq!( <&RoomAliasId>::try_from("39hvsi03hlne:example.com").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn missing_room_alias_id_delimiter() { assert_eq!(<&RoomAliasId>::try_from("#ruma").unwrap_err(), IdParseError::MissingColon); } #[test] fn invalid_leading_sigil() { assert_eq!( <&RoomAliasId>::try_from("!room_id:foo.bar").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn invalid_room_alias_id_host() { assert_eq!( <&RoomAliasId>::try_from("#ruma:/").unwrap_err(), IdParseError::InvalidServerName ); } #[test] fn invalid_room_alias_id_port() { assert_eq!( <&RoomAliasId>::try_from("#ruma:example.com:notaport").unwrap_err(), IdParseError::InvalidServerName ); } } ruma-common-0.10.5/src/identifiers/room_id.rs000064400000000000000000000243441046102023000172420ustar 00000000000000//! Matrix room identifiers. use ruma_macros::IdZst; use super::{ matrix_uri::UriAction, MatrixToUri, MatrixUri, OwnedEventId, OwnedServerName, ServerName, }; /// A Matrix [room ID]. /// /// A `RoomId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// /// ``` /// # use ruma_common::RoomId; /// assert_eq!(<&RoomId>::try_from("!n8f893n9:example.com").unwrap(), "!n8f893n9:example.com"); /// ``` /// /// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::room_id::validate)] pub struct RoomId(str); impl RoomId { /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of /// 18 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new(server_name: &ServerName) -> OwnedRoomId { Self::from_borrowed(&format!("!{}:{}", super::generate_localpart(18), server_name)) .to_owned() } /// Returns the rooms's unique ID. pub fn localpart(&self) -> &str { &self.as_str()[1..self.colon_idx()] } /// Returns the server name of the room ID. pub fn server_name(&self) -> &ServerName { ServerName::from_borrowed(&self.as_str()[self.colon_idx() + 1..]) } /// Create a `matrix.to` URI for this room ID. /// /// Note that it is recommended to provide servers that should know the room to be able to find /// it with its room ID. For that use [`RoomId::matrix_to_uri_via()`]. /// /// # Example /// /// ``` /// use ruma_common::{room_id, server_name}; /// /// assert_eq!( /// room_id!("!somewhere:example.org").matrix_to_uri().to_string(), /// "https://matrix.to/#/%21somewhere%3Aexample.org" /// ); /// ``` pub fn matrix_to_uri(&self) -> MatrixToUri { MatrixToUri::new(self.into(), vec![]) } /// Create a `matrix.to` URI for this room ID with a list of servers that should know it. /// /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec. /// /// If you don't have a list of servers, you can use [`RoomId::matrix_to_uri()`] instead. /// /// # Example /// /// ``` /// use ruma_common::{room_id, server_name}; /// /// assert_eq!( /// room_id!("!somewhere:example.org") /// .matrix_to_uri_via([&*server_name!("example.org"), &*server_name!("alt.example.org")]) /// .to_string(), /// "https://matrix.to/#/%21somewhere%3Aexample.org?via=example.org&via=alt.example.org" /// ); /// ``` /// /// [routing algorithm]: https://spec.matrix.org/v1.3/appendices/#routing pub fn matrix_to_uri_via(&self, via: T) -> MatrixToUri where T: IntoIterator, T::Item: Into, { MatrixToUri::new(self.into(), via.into_iter().map(Into::into).collect()) } /// Create a `matrix.to` URI for an event scoped under this room ID. /// /// Note that it is recommended to provide servers that should know the room to be able to find /// it with its room ID. For that use [`RoomId::matrix_to_event_uri_via()`]. pub fn matrix_to_event_uri(&self, ev_id: impl Into) -> MatrixToUri { MatrixToUri::new((self.to_owned(), ev_id.into()).into(), vec![]) } /// Create a `matrix.to` URI for an event scoped under this room ID with a list of servers that /// should know it. /// /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec. /// /// If you don't have a list of servers, you can use [`RoomId::matrix_to_event_uri()`] instead. /// /// [routing algorithm]: https://spec.matrix.org/v1.3/appendices/#routing pub fn matrix_to_event_uri_via(&self, ev_id: impl Into, via: T) -> MatrixToUri where T: IntoIterator, T::Item: Into, { MatrixToUri::new( (self.to_owned(), ev_id.into()).into(), via.into_iter().map(Into::into).collect(), ) } /// Create a `matrix:` URI for this room ID. /// /// If `join` is `true`, a click on the URI should join the room. /// /// Note that it is recommended to provide servers that should know the room to be able to find /// it with its room ID. For that use [`RoomId::matrix_uri_via()`]. /// /// # Example /// /// ``` /// use ruma_common::{room_id, server_name}; /// /// assert_eq!( /// room_id!("!somewhere:example.org").matrix_uri(false).to_string(), /// "matrix:roomid/somewhere:example.org" /// ); /// ``` pub fn matrix_uri(&self, join: bool) -> MatrixUri { MatrixUri::new(self.into(), vec![], Some(UriAction::Join).filter(|_| join)) } /// Create a `matrix:` URI for this room ID with a list of servers that should know it. /// /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec. /// /// If you don't have a list of servers, you can use [`RoomId::matrix_uri()`] instead. /// /// If `join` is `true`, a click on the URI should join the room. /// /// # Example /// /// ``` /// use ruma_common::{room_id, server_name}; /// /// assert_eq!( /// room_id!("!somewhere:example.org") /// .matrix_uri_via( /// [&*server_name!("example.org"), &*server_name!("alt.example.org")], /// true /// ) /// .to_string(), /// "matrix:roomid/somewhere:example.org?via=example.org&via=alt.example.org&action=join" /// ); /// ``` /// /// [routing algorithm]: https://spec.matrix.org/v1.3/appendices/#routing pub fn matrix_uri_via(&self, via: T, join: bool) -> MatrixUri where T: IntoIterator, T::Item: Into, { MatrixUri::new( self.into(), via.into_iter().map(Into::into).collect(), Some(UriAction::Join).filter(|_| join), ) } /// Create a `matrix:` URI for an event scoped under this room ID. /// /// Note that it is recommended to provide servers that should know the room to be able to find /// it with its room ID. For that use [`RoomId::matrix_event_uri_via()`]. pub fn matrix_event_uri(&self, ev_id: impl Into) -> MatrixUri { MatrixUri::new((self.to_owned(), ev_id.into()).into(), vec![], None) } /// Create a `matrix:` URI for an event scoped under this room ID with a list of servers that /// should know it. /// /// To get the list of servers, it is recommended to use the [routing algorithm] from the spec. /// /// If you don't have a list of servers, you can use [`RoomId::matrix_event_uri()`] instead. /// /// [routing algorithm]: https://spec.matrix.org/v1.3/appendices/#routing pub fn matrix_event_uri_via(&self, ev_id: impl Into, via: T) -> MatrixUri where T: IntoIterator, T::Item: Into, { MatrixUri::new( (self.to_owned(), ev_id.into()).into(), via.into_iter().map(Into::into).collect(), None, ) } fn colon_idx(&self) -> usize { self.as_str().find(':').unwrap() } } #[cfg(test)] mod tests { use super::{OwnedRoomId, RoomId}; use crate::IdParseError; #[test] fn valid_room_id() { assert_eq!( <&RoomId>::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") .as_ref(), "!29fhd83h92h0:example.com" ); } #[test] fn empty_localpart() { assert_eq!( <&RoomId>::try_from("!:example.com").expect("Failed to create RoomId.").as_ref(), "!:example.com" ); } #[cfg(feature = "rand")] #[test] fn generate_random_valid_room_id() { use crate::server_name; let room_id = RoomId::new(server_name!("example.com")); let id_str = room_id.as_str(); assert!(id_str.starts_with('!')); assert_eq!(id_str.len(), 31); } #[test] fn serialize_valid_room_id() { assert_eq!( serde_json::to_string( <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") ) .expect("Failed to convert RoomId to JSON."), r#""!29fhd83h92h0:example.com""# ); } #[test] fn deserialize_valid_room_id() { assert_eq!( serde_json::from_str::(r#""!29fhd83h92h0:example.com""#) .expect("Failed to convert JSON to RoomId"), <&RoomId>::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") ); } #[test] fn valid_room_id_with_explicit_standard_port() { assert_eq!( <&RoomId>::try_from("!29fhd83h92h0:example.com:443") .expect("Failed to create RoomId.") .as_ref(), "!29fhd83h92h0:example.com:443" ); } #[test] fn valid_room_id_with_non_standard_port() { assert_eq!( <&RoomId>::try_from("!29fhd83h92h0:example.com:5000") .expect("Failed to create RoomId.") .as_ref(), "!29fhd83h92h0:example.com:5000" ); } #[test] fn missing_room_id_sigil() { assert_eq!( <&RoomId>::try_from("carl:example.com").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn missing_room_id_delimiter() { assert_eq!(<&RoomId>::try_from("!29fhd83h92h0").unwrap_err(), IdParseError::MissingColon); } #[test] fn invalid_room_id_host() { assert_eq!( <&RoomId>::try_from("!29fhd83h92h0:/").unwrap_err(), IdParseError::InvalidServerName ); } #[test] fn invalid_room_id_port() { assert_eq!( <&RoomId>::try_from("!29fhd83h92h0:example.com:notaport").unwrap_err(), IdParseError::InvalidServerName ); } } ruma-common-0.10.5/src/identifiers/room_or_room_alias_id.rs000064400000000000000000000156571046102023000221560ustar 00000000000000//! Matrix identifiers for places where a room ID or room alias ID are used interchangeably. use std::hint::unreachable_unchecked; use ruma_macros::IdZst; use super::{server_name::ServerName, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId}; /// A Matrix [room ID] or a Matrix [room alias ID]. /// /// `RoomOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted /// from a string slice, and can be converted back into a string as needed. When converted from a /// string slice, the variant is determined by the leading sigil character. /// /// ``` /// # use ruma_common::RoomOrAliasId; /// assert_eq!(<&RoomOrAliasId>::try_from("#ruma:example.com").unwrap(), "#ruma:example.com"); /// /// assert_eq!( /// <&RoomOrAliasId>::try_from("!n8f893n9:example.com").unwrap(), /// "!n8f893n9:example.com" /// ); /// ``` /// /// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids /// [room alias ID]: https://spec.matrix.org/v1.2/appendices/#room-aliases #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::room_id_or_alias_id::validate)] pub struct RoomOrAliasId(str); impl RoomOrAliasId { /// Returns the local part (everything after the `!` or `#` and before the first colon). pub fn localpart(&self) -> &str { &self.as_str()[1..self.colon_idx()] } /// Returns the server name of the room (alias) ID. pub fn server_name(&self) -> &ServerName { ServerName::from_borrowed(&self.as_str()[self.colon_idx() + 1..]) } /// Whether this is a room id (starts with `'!'`) pub fn is_room_id(&self) -> bool { self.variant() == Variant::RoomId } /// Whether this is a room alias id (starts with `'#'`) pub fn is_room_alias_id(&self) -> bool { self.variant() == Variant::RoomAliasId } fn colon_idx(&self) -> usize { self.as_str().find(':').unwrap() } fn variant(&self) -> Variant { match self.as_str().bytes().next() { Some(b'!') => Variant::RoomId, Some(b'#') => Variant::RoomAliasId, _ => unsafe { unreachable_unchecked() }, } } } #[derive(PartialEq, Eq)] enum Variant { RoomId, RoomAliasId, } impl<'a> From<&'a RoomId> for &'a RoomOrAliasId { fn from(room_id: &'a RoomId) -> Self { RoomOrAliasId::from_borrowed(room_id.as_str()) } } impl<'a> From<&'a RoomAliasId> for &'a RoomOrAliasId { fn from(room_alias_id: &'a RoomAliasId) -> Self { RoomOrAliasId::from_borrowed(room_alias_id.as_str()) } } impl From for OwnedRoomOrAliasId { fn from(room_id: OwnedRoomId) -> Self { // FIXME: Don't allocate RoomOrAliasId::from_borrowed(room_id.as_str()).to_owned() } } impl From for OwnedRoomOrAliasId { fn from(room_alias_id: OwnedRoomAliasId) -> Self { // FIXME: Don't allocate RoomOrAliasId::from_borrowed(room_alias_id.as_str()).to_owned() } } impl<'a> TryFrom<&'a RoomOrAliasId> for &'a RoomId { type Error = &'a RoomAliasId; fn try_from(id: &'a RoomOrAliasId) -> Result<&'a RoomId, &'a RoomAliasId> { match id.variant() { Variant::RoomId => Ok(RoomId::from_borrowed(id.as_str())), Variant::RoomAliasId => Err(RoomAliasId::from_borrowed(id.as_str())), } } } impl<'a> TryFrom<&'a RoomOrAliasId> for &'a RoomAliasId { type Error = &'a RoomId; fn try_from(id: &'a RoomOrAliasId) -> Result<&'a RoomAliasId, &'a RoomId> { match id.variant() { Variant::RoomAliasId => Ok(RoomAliasId::from_borrowed(id.as_str())), Variant::RoomId => Err(RoomId::from_borrowed(id.as_str())), } } } impl TryFrom for OwnedRoomId { type Error = OwnedRoomAliasId; fn try_from(id: OwnedRoomOrAliasId) -> Result { // FIXME: Don't allocate match id.variant() { Variant::RoomId => Ok(RoomId::from_borrowed(id.as_str()).to_owned()), Variant::RoomAliasId => Err(RoomAliasId::from_borrowed(id.as_str()).to_owned()), } } } impl TryFrom for OwnedRoomAliasId { type Error = OwnedRoomId; fn try_from(id: OwnedRoomOrAliasId) -> Result { // FIXME: Don't allocate match id.variant() { Variant::RoomAliasId => Ok(RoomAliasId::from_borrowed(id.as_str()).to_owned()), Variant::RoomId => Err(RoomId::from_borrowed(id.as_str()).to_owned()), } } } #[cfg(test)] mod tests { use super::{OwnedRoomOrAliasId, RoomOrAliasId}; use crate::IdParseError; #[test] fn valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( <&RoomOrAliasId>::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") .as_ref(), "#ruma:example.com" ); } #[test] fn valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( <&RoomOrAliasId>::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") .as_ref(), "!29fhd83h92h0:example.com" ); } #[test] fn missing_sigil_for_room_id_or_alias_id() { assert_eq!( <&RoomOrAliasId>::try_from("ruma:example.com").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( serde_json::to_string( <&RoomOrAliasId>::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") ) .expect("Failed to convert RoomAliasId to JSON."), r##""#ruma:example.com""## ); } #[test] fn serialize_valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( serde_json::to_string( <&RoomOrAliasId>::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") ) .expect("Failed to convert RoomId to JSON."), r#""!29fhd83h92h0:example.com""# ); } #[test] fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( serde_json::from_str::(r##""#ruma:example.com""##) .expect("Failed to convert JSON to RoomAliasId"), <&RoomOrAliasId>::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") ); } #[test] fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( serde_json::from_str::(r##""!29fhd83h92h0:example.com""##) .expect("Failed to convert JSON to RoomId"), <&RoomOrAliasId>::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomAliasId.") ); } } ruma-common-0.10.5/src/identifiers/room_version_id.rs000064400000000000000000000235371046102023000210120ustar 00000000000000//! Matrix room version identifiers. use std::{cmp::Ordering, str::FromStr}; use ruma_macros::DisplayAsRefStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::IdParseError; /// A Matrix [room version] ID. /// /// A `RoomVersionId` can be or converted or deserialized from a string slice, and can be converted /// or serialized back into a string as needed. /// /// ``` /// # use ruma_common::RoomVersionId; /// assert_eq!(RoomVersionId::try_from("1").unwrap().as_ref(), "1"); /// ``` /// /// Any string consisting of at minimum 1, at maximum 32 unicode codepoints is a room version ID. /// Custom room versions or ones that were introduced into the specification after this code was /// written are represented by a hidden enum variant. You can still construct them the same, and /// check for them using one of `RoomVersionId`s `PartialEq` implementations or through `.as_str()`. /// /// [room version]: https://spec.matrix.org/v1.2/rooms/ #[derive(Clone, Debug, PartialEq, Eq, Hash, DisplayAsRefStr)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum RoomVersionId { /// A version 1 room. V1, /// A version 2 room. V2, /// A version 3 room. V3, /// A version 4 room. V4, /// A version 5 room. V5, /// A version 6 room. V6, /// A version 7 room. V7, /// A version 8 room. V8, /// A version 9 room. V9, /// A version 10 room. V10, #[doc(hidden)] _Custom(CustomRoomVersion), } impl RoomVersionId { /// Creates a string slice from this `RoomVersionId`. pub fn as_str(&self) -> &str { // FIXME: Add support for non-`str`-deref'ing types for fallback to AsRefStr derive and // implement this function in terms of `AsRef` match &self { Self::V1 => "1", Self::V2 => "2", Self::V3 => "3", Self::V4 => "4", Self::V5 => "5", Self::V6 => "6", Self::V7 => "7", Self::V8 => "8", Self::V9 => "9", Self::V10 => "10", Self::_Custom(version) => version.as_str(), } } /// Creates a byte slice from this `RoomVersionId`. pub fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } } impl From for String { fn from(id: RoomVersionId) -> Self { match id { RoomVersionId::V1 => "1".to_owned(), RoomVersionId::V2 => "2".to_owned(), RoomVersionId::V3 => "3".to_owned(), RoomVersionId::V4 => "4".to_owned(), RoomVersionId::V5 => "5".to_owned(), RoomVersionId::V6 => "6".to_owned(), RoomVersionId::V7 => "7".to_owned(), RoomVersionId::V8 => "8".to_owned(), RoomVersionId::V9 => "9".to_owned(), RoomVersionId::V10 => "10".to_owned(), RoomVersionId::_Custom(version) => version.into(), } } } impl AsRef for RoomVersionId { fn as_ref(&self) -> &str { self.as_str() } } impl PartialOrd for RoomVersionId { /// Compare the two given room version IDs by comparing their string representations. /// /// Please be aware that room version IDs don't have a defined ordering in the Matrix /// specification. This implementation only exists to be able to use `RoomVersionId`s or /// types containing `RoomVersionId`s as `BTreeMap` keys. fn partial_cmp(&self, other: &RoomVersionId) -> Option { self.as_ref().partial_cmp(other.as_ref()) } } impl Ord for RoomVersionId { /// Compare the two given room version IDs by comparing their string representations. /// /// Please be aware that room version IDs don't have a defined ordering in the Matrix /// specification. This implementation only exists to be able to use `RoomVersionId`s or /// types containing `RoomVersionId`s as `BTreeMap` keys. fn cmp(&self, other: &Self) -> Ordering { self.as_ref().cmp(other.as_ref()) } } impl Serialize for RoomVersionId { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(self.as_ref()) } } impl<'de> Deserialize<'de> for RoomVersionId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { super::deserialize_id(deserializer, "a Matrix room version ID as a string") } } /// Attempts to create a new Matrix room version ID from a string representation. fn try_from(room_version_id: S) -> Result where S: AsRef + Into>, { let version = match room_version_id.as_ref() { "1" => RoomVersionId::V1, "2" => RoomVersionId::V2, "3" => RoomVersionId::V3, "4" => RoomVersionId::V4, "5" => RoomVersionId::V5, "6" => RoomVersionId::V6, "7" => RoomVersionId::V7, "8" => RoomVersionId::V8, "9" => RoomVersionId::V9, "10" => RoomVersionId::V10, custom => { ruma_identifiers_validation::room_version_id::validate(custom)?; RoomVersionId::_Custom(CustomRoomVersion(room_version_id.into())) } }; Ok(version) } impl FromStr for RoomVersionId { type Err = IdParseError; fn from_str(s: &str) -> Result { try_from(s) } } impl TryFrom<&str> for RoomVersionId { type Error = IdParseError; fn try_from(s: &str) -> Result { try_from(s) } } impl TryFrom for RoomVersionId { type Error = IdParseError; fn try_from(s: String) -> Result { try_from(s) } } impl PartialEq<&str> for RoomVersionId { fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq for &str { fn eq(&self, other: &RoomVersionId) -> bool { *self == other.as_str() } } impl PartialEq for RoomVersionId { fn eq(&self, other: &String) -> bool { self.as_str() == other } } impl PartialEq for String { fn eq(&self, other: &RoomVersionId) -> bool { self == other.as_str() } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] #[doc(hidden)] pub struct CustomRoomVersion(Box); #[doc(hidden)] impl CustomRoomVersion { /// Creates a string slice from this `CustomRoomVersion` pub fn as_str(&self) -> &str { &self.0 } } #[doc(hidden)] impl From for String { fn from(v: CustomRoomVersion) -> Self { v.0.into() } } #[doc(hidden)] impl AsRef for CustomRoomVersion { fn as_ref(&self) -> &str { self.as_str() } } #[cfg(test)] mod tests { use super::RoomVersionId; use crate::IdParseError; #[test] fn valid_version_1_room_version_id() { assert_eq!( RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.").as_ref(), "1" ); } #[test] fn valid_version_2_room_version_id() { assert_eq!( RoomVersionId::try_from("2").expect("Failed to create RoomVersionId.").as_ref(), "2" ); } #[test] fn valid_version_3_room_version_id() { assert_eq!( RoomVersionId::try_from("3").expect("Failed to create RoomVersionId.").as_ref(), "3" ); } #[test] fn valid_version_4_room_version_id() { assert_eq!( RoomVersionId::try_from("4").expect("Failed to create RoomVersionId.").as_ref(), "4" ); } #[test] fn valid_version_5_room_version_id() { assert_eq!( RoomVersionId::try_from("5").expect("Failed to create RoomVersionId.").as_ref(), "5" ); } #[test] fn valid_version_6_room_version_id() { assert_eq!( RoomVersionId::try_from("6").expect("Failed to create RoomVersionId.").as_ref(), "6" ); } #[test] fn valid_custom_room_version_id() { assert_eq!( RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.").as_ref(), "io.ruma.1" ); } #[test] fn empty_room_version_id() { assert_eq!(RoomVersionId::try_from(""), Err(IdParseError::Empty)); } #[test] fn over_max_code_point_room_version_id() { assert_eq!( RoomVersionId::try_from("0123456789012345678901234567890123456789"), Err(IdParseError::MaximumLengthExceeded) ); } #[test] fn serialize_official_room_id() { assert_eq!( serde_json::to_string( &RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.") ) .expect("Failed to convert RoomVersionId to JSON."), r#""1""# ); } #[test] fn deserialize_official_room_id() { let deserialized = serde_json::from_str::(r#""1""#) .expect("Failed to convert RoomVersionId to JSON."); assert_eq!(deserialized, RoomVersionId::V1); assert_eq!( deserialized, RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.") ); } #[test] fn serialize_custom_room_id() { assert_eq!( serde_json::to_string( &RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") ) .expect("Failed to convert RoomVersionId to JSON."), r#""io.ruma.1""# ); } #[test] fn deserialize_custom_room_id() { let deserialized = serde_json::from_str::(r#""io.ruma.1""#) .expect("Failed to convert RoomVersionId to JSON."); assert_eq!( deserialized, RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") ); } } ruma-common-0.10.5/src/identifiers/server_name.rs000064400000000000000000000103471046102023000201160ustar 00000000000000//! Matrix-spec compliant server names. use std::net::Ipv4Addr; use ruma_macros::IdZst; /// A Matrix-spec compliant [server name]. /// /// It consists of a host and an optional port (separated by a colon if present). /// /// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::server_name::validate)] pub struct ServerName(str); impl ServerName { /// Returns the host of the server name. /// /// That is: Return the part of the server name before `:` or the full server name if /// there is no port. pub fn host(&self) -> &str { if let Some(end_of_ipv6) = self.0.find(']') { &self.0[..=end_of_ipv6] } else { // It's not ipv6, so ':' means the port starts let end_of_host = self.0.find(':').unwrap_or(self.0.len()); &self.0[..end_of_host] } } /// Returns the port of the server name, if any. pub fn port(&self) -> Option { #[allow(clippy::unnecessary_lazy_evaluations)] let end_of_host = self .0 .find(']') .map(|i| i + 1) .or_else(|| self.0.find(':')) .unwrap_or_else(|| self.0.len()); (self.0.len() != end_of_host).then(|| { assert!(self.as_bytes()[end_of_host] == b':'); self.0[end_of_host + 1..].parse().unwrap() }) } /// Returns true if and only if the server name is an IPv4 or IPv6 address. pub fn is_ip_literal(&self) -> bool { self.host().parse::().is_ok() || self.0.starts_with('[') } } #[cfg(test)] mod tests { use super::ServerName; #[test] fn ipv4_host() { <&ServerName>::try_from("127.0.0.1").unwrap(); } #[test] fn ipv4_host_and_port() { <&ServerName>::try_from("1.1.1.1:12000").unwrap(); } #[test] fn ipv6() { <&ServerName>::try_from("[::1]").unwrap(); } #[test] fn ipv6_with_port() { <&ServerName>::try_from("[1234:5678::abcd]:5678").unwrap(); } #[test] fn dns_name() { <&ServerName>::try_from("example.com").unwrap(); } #[test] fn dns_name_with_port() { <&ServerName>::try_from("ruma.io:8080").unwrap(); } #[test] fn empty_string() { <&ServerName>::try_from("").unwrap_err(); } #[test] fn invalid_ipv6() { <&ServerName>::try_from("[test::1]").unwrap_err(); } #[test] fn ipv4_with_invalid_port() { <&ServerName>::try_from("127.0.0.1:").unwrap_err(); } #[test] fn ipv6_with_invalid_port() { <&ServerName>::try_from("[fe80::1]:100000").unwrap_err(); <&ServerName>::try_from("[fe80::1]!").unwrap_err(); } #[test] fn dns_name_with_invalid_port() { <&ServerName>::try_from("matrix.org:hello").unwrap_err(); } #[test] fn parse_ipv4_host() { let server_name = <&ServerName>::try_from("127.0.0.1").unwrap(); assert!(server_name.is_ip_literal()); assert_eq!(server_name.host(), "127.0.0.1"); } #[test] fn parse_ipv4_host_and_port() { let server_name = <&ServerName>::try_from("1.1.1.1:12000").unwrap(); assert!(server_name.is_ip_literal()); assert_eq!(server_name.host(), "1.1.1.1"); } #[test] fn parse_ipv6() { let server_name = <&ServerName>::try_from("[::1]").unwrap(); assert!(server_name.is_ip_literal()); assert_eq!(server_name.host(), "[::1]"); } #[test] fn parse_ipv6_with_port() { let server_name = <&ServerName>::try_from("[1234:5678::abcd]:5678").unwrap(); assert!(server_name.is_ip_literal()); assert_eq!(server_name.host(), "[1234:5678::abcd]"); } #[test] fn parse_dns_name() { let server_name = <&ServerName>::try_from("example.com").unwrap(); assert!(!server_name.is_ip_literal()); assert_eq!(server_name.host(), "example.com"); } #[test] fn parse_dns_name_with_port() { let server_name = <&ServerName>::try_from("ruma.io:8080").unwrap(); assert!(!server_name.is_ip_literal()); assert_eq!(server_name.host(), "ruma.io"); } } ruma-common-0.10.5/src/identifiers/session_id.rs000064400000000000000000000034151046102023000177450ustar 00000000000000//! Matrix session ID. use ruma_macros::IdZst; use super::IdParseError; /// A session ID. /// /// Session IDs in Matrix are opaque character sequences of `[0-9a-zA-Z.=_-]`. Their length must /// must not exceed 255 characters. #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = validate_session_id)] pub struct SessionId(str); impl SessionId { #[doc(hidden)] pub const fn _priv_const_new(s: &str) -> Result<&Self, &'static str> { match validate_session_id(s) { Ok(()) => Ok(Self::from_borrowed(s)), Err(IdParseError::MaximumLengthExceeded) => { Err("Invalid Session ID: exceeds 255 bytes") } Err(IdParseError::InvalidCharacters) => { Err("Invalid Session ID: contains invalid characters") } Err(IdParseError::Empty) => Err("Invalid Session ID: empty"), Err(_) => unreachable!(), } } } const fn validate_session_id(s: &str) -> Result<(), IdParseError> { if s.len() > 255 { return Err(IdParseError::MaximumLengthExceeded); } else if contains_invalid_byte(s.as_bytes()) { return Err(IdParseError::InvalidCharacters); } else if s.is_empty() { return Err(IdParseError::Empty); } Ok(()) } const fn contains_invalid_byte(mut bytes: &[u8]) -> bool { // non-const form: // // bytes.iter().all(|b| b.is_ascii_alphanumeric() || b".=_-".contains(&b)) loop { if let Some((byte, rest)) = bytes.split_first() { if byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'=' | b'_' | b'-') { bytes = rest; } else { break true; } } else { break false; } } } ruma-common-0.10.5/src/identifiers/signatures.rs000064400000000000000000000037121046102023000177720ustar 00000000000000use std::{borrow::Borrow, collections::BTreeMap}; use serde::{Deserialize, Serialize}; use super::{OwnedDeviceId, OwnedKeyName, OwnedServerName, OwnedSigningKeyId, OwnedUserId}; /// Map of key identifier to signature values. pub type EntitySignatures = BTreeMap, String>; /// Map of all signatures, grouped by entity /// /// ``` /// # use ruma_common::{server_name, KeyId, Signatures, SigningKeyAlgorithm}; /// let key_identifier = KeyId::from_parts(SigningKeyAlgorithm::Ed25519, "1"); /// let mut signatures = Signatures::new(); /// let server_name = server_name!("example.org"); /// let signature = /// "YbJva03ihSj5mPk+CHMJKUKlCXCPFXjXOK6VqBnN9nA2evksQcTGn6hwQfrgRHIDDXO2le49x7jnWJHMJrJoBQ"; /// signatures.insert(server_name, key_identifier, signature.into()); /// ``` #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct Signatures(BTreeMap>); impl Signatures { /// Creates an empty signature map. pub fn new() -> Self { Self(BTreeMap::new()) } /// Add a signature for the given server name and key identifier. /// /// If there was already one, it is returned. pub fn insert( &mut self, entity: E, key_identifier: OwnedSigningKeyId, value: String, ) -> Option { self.0.entry(entity).or_insert_with(Default::default).insert(key_identifier, value) } /// Returns a reference to the signatures corresponding to the entities. pub fn get(&self, entity: &Q) -> Option<&EntitySignatures> where E: Borrow, Q: Ord + ?Sized, { self.0.get(entity) } } /// Map of server signatures for an event, grouped by server. pub type ServerSignatures = Signatures; /// Map of device signatures for an event, grouped by user. pub type DeviceSignatures = Signatures; ruma-common-0.10.5/src/identifiers/transaction_id.rs000064400000000000000000000017131046102023000206060ustar 00000000000000use ruma_macros::IdZst; /// A Matrix transaction ID. /// /// Transaction IDs in Matrix are opaque strings. This type is provided simply for its semantic /// value. /// /// You can create one from a string (using `.into()`) but the recommended way is to use /// `TransactionId::new()` to generate a random one. If that function is not available for you, you /// need to activate this crate's `rand` Cargo feature. #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] pub struct TransactionId(str); impl TransactionId { /// Creates a random transaction ID. /// /// This will currently be a UUID without hyphens, but no guarantees are made about the /// structure of transaction IDs generated from this function. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new() -> OwnedTransactionId { let id = uuid::Uuid::new_v4(); Self::from_borrowed(&id.simple().to_string()).to_owned() } } ruma-common-0.10.5/src/identifiers/user_id.rs000064400000000000000000000263341046102023000172450ustar 00000000000000//! Matrix user identifiers. use std::{rc::Rc, sync::Arc}; use super::{matrix_uri::UriAction, IdParseError, MatrixToUri, MatrixUri, ServerName}; /// A Matrix [user ID]. /// /// A `UserId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// /// ``` /// # use ruma_common::UserId; /// assert_eq!(<&UserId>::try_from("@carl:example.com").unwrap(), "@carl:example.com"); /// ``` /// /// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] #[ruma_id(validate = ruma_identifiers_validation::user_id::validate)] pub struct UserId(str); impl UserId { /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of /// 12 random ASCII characters. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new(server_name: &ServerName) -> OwnedUserId { Self::from_borrowed(&format!( "@{}:{}", super::generate_localpart(12).to_lowercase(), server_name )) .to_owned() } /// Attempts to complete a user ID, by adding the colon + server name and `@` prefix, if not /// present already. /// /// This is a convenience function for the login API, where a user can supply either their full /// user ID or just the localpart. It only supports a valid user ID or a valid user ID /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without /// the `@` prefix. pub fn parse_with_server_name( id: impl AsRef + Into>, server_name: &ServerName, ) -> Result { let id_str = id.as_ref(); if id_str.starts_with('@') { Self::parse(id).map(Into::into) } else { let _ = localpart_is_fully_conforming(id_str)?; Ok(Self::from_borrowed(&format!("@{id_str}:{server_name}")).to_owned()) } } /// Variation of [`parse_with_server_name`] that returns `Rc`. /// /// [`parse_with_server_name`]: Self::parse_with_server_name pub fn parse_with_server_name_rc( id: impl AsRef + Into>, server_name: &ServerName, ) -> Result, IdParseError> { let id_str = id.as_ref(); if id_str.starts_with('@') { Self::parse_rc(id) } else { let _ = localpart_is_fully_conforming(id_str)?; Ok(Self::from_rc(format!("@{id_str}:{server_name}").into())) } } /// Variation of [`parse_with_server_name`] that returns `Arc`. /// /// [`parse_with_server_name`]: Self::parse_with_server_name pub fn parse_with_server_name_arc( id: impl AsRef + Into>, server_name: &ServerName, ) -> Result, IdParseError> { let id_str = id.as_ref(); if id_str.starts_with('@') { Self::parse_arc(id) } else { let _ = localpart_is_fully_conforming(id_str)?; Ok(Self::from_arc(format!("@{id_str}:{server_name}").into())) } } /// Returns the user's localpart. pub fn localpart(&self) -> &str { &self.as_str()[1..self.colon_idx() as usize] } /// Returns the server name of the user ID. pub fn server_name(&self) -> &ServerName { ServerName::from_borrowed(&self.as_str()[self.colon_idx() + 1..]) } /// Whether this user ID is a historical one. /// /// A historical user ID is one that doesn't conform to the latest specification of the user ID /// grammar but is still accepted because it was previously allowed. pub fn is_historical(&self) -> bool { !localpart_is_fully_conforming(self.localpart()).unwrap() } /// Create a `matrix.to` URI for this user ID. /// /// # Example /// /// ``` /// use ruma_common::user_id; /// /// let message = format!( /// r#"Thanks for the update {display_name}."#, /// link = user_id!("@jplatte:notareal.hs").matrix_to_uri(), /// display_name = "jplatte", /// ); /// ``` pub fn matrix_to_uri(&self) -> MatrixToUri { MatrixToUri::new(self.into(), Vec::new()) } /// Create a `matrix:` URI for this user ID. /// /// If `chat` is `true`, a click on the URI should start a direct message /// with the user. /// /// # Example /// /// ``` /// use ruma_common::user_id; /// /// let message = format!( /// r#"Thanks for the update {display_name}."#, /// link = user_id!("@jplatte:notareal.hs").matrix_uri(false), /// display_name = "jplatte", /// ); /// ``` pub fn matrix_uri(&self, chat: bool) -> MatrixUri { MatrixUri::new(self.into(), Vec::new(), Some(UriAction::Chat).filter(|_| chat)) } fn colon_idx(&self) -> usize { self.as_str().find(':').unwrap() } } pub use ruma_identifiers_validation::user_id::localpart_is_fully_conforming; use ruma_macros::IdZst; #[cfg(test)] mod tests { use super::{OwnedUserId, UserId}; use crate::{server_name, IdParseError}; #[test] fn valid_user_id_from_str() { let user_id = <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@carl:example.com"); assert_eq!(user_id.localpart(), "carl"); assert_eq!(user_id.server_name(), "example.com"); assert!(!user_id.is_historical()); } #[test] fn parse_valid_user_id() { let server_name = server_name!("example.com"); let user_id = UserId::parse_with_server_name("@carl:example.com", server_name) .expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@carl:example.com"); assert_eq!(user_id.localpart(), "carl"); assert_eq!(user_id.server_name(), "example.com"); assert!(!user_id.is_historical()); } #[test] fn parse_valid_user_id_parts() { let server_name = server_name!("example.com"); let user_id = UserId::parse_with_server_name("carl", server_name).expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@carl:example.com"); assert_eq!(user_id.localpart(), "carl"); assert_eq!(user_id.server_name(), "example.com"); assert!(!user_id.is_historical()); } #[cfg(not(feature = "compat"))] #[test] fn invalid_user_id() { let localpart = "τ"; let user_id = "@τ:example.com"; let server_name = server_name!("example.com"); <&UserId>::try_from(user_id).unwrap_err(); UserId::parse_with_server_name(user_id, server_name).unwrap_err(); UserId::parse_with_server_name(localpart, server_name).unwrap_err(); UserId::parse_with_server_name_rc(user_id, server_name).unwrap_err(); UserId::parse_with_server_name_rc(localpart, server_name).unwrap_err(); UserId::parse_with_server_name_arc(user_id, server_name).unwrap_err(); UserId::parse_with_server_name_arc(localpart, server_name).unwrap_err(); UserId::parse_rc(user_id).unwrap_err(); UserId::parse_arc(user_id).unwrap_err(); } #[test] fn valid_historical_user_id() { let user_id = <&UserId>::try_from("@a%b[irc]:example.com").expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@a%b[irc]:example.com"); assert_eq!(user_id.localpart(), "a%b[irc]"); assert_eq!(user_id.server_name(), "example.com"); assert!(user_id.is_historical()); } #[test] fn parse_valid_historical_user_id() { let server_name = server_name!("example.com"); let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", server_name) .expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@a%b[irc]:example.com"); assert_eq!(user_id.localpart(), "a%b[irc]"); assert_eq!(user_id.server_name(), "example.com"); assert!(user_id.is_historical()); } #[test] fn parse_valid_historical_user_id_parts() { let server_name = server_name!("example.com"); let user_id = UserId::parse_with_server_name("a%b[irc]", server_name) .expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@a%b[irc]:example.com"); assert_eq!(user_id.localpart(), "a%b[irc]"); assert_eq!(user_id.server_name(), "example.com"); assert!(user_id.is_historical()); } #[test] fn uppercase_user_id() { let user_id = <&UserId>::try_from("@CARL:example.com").expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@CARL:example.com"); assert!(user_id.is_historical()); } #[cfg(feature = "rand")] #[test] fn generate_random_valid_user_id() { let server_name = server_name!("example.com"); let user_id = UserId::new(server_name); assert_eq!(user_id.localpart().len(), 12); assert_eq!(user_id.server_name(), "example.com"); let id_str = user_id.as_str(); assert!(id_str.starts_with('@')); assert_eq!(id_str.len(), 25); } #[test] fn serialize_valid_user_id() { assert_eq!( serde_json::to_string( <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.") ) .expect("Failed to convert UserId to JSON."), r#""@carl:example.com""# ); } #[test] fn deserialize_valid_user_id() { assert_eq!( serde_json::from_str::(r#""@carl:example.com""#) .expect("Failed to convert JSON to UserId"), <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.") ); } #[test] fn valid_user_id_with_explicit_standard_port() { assert_eq!( <&UserId>::try_from("@carl:example.com:443") .expect("Failed to create UserId.") .as_ref(), "@carl:example.com:443" ); } #[test] fn valid_user_id_with_non_standard_port() { let user_id = <&UserId>::try_from("@carl:example.com:5000").expect("Failed to create UserId."); assert_eq!(user_id.as_str(), "@carl:example.com:5000"); assert!(!user_id.is_historical()); } #[test] #[cfg(not(feature = "compat"))] fn invalid_characters_in_user_id_localpart() { assert_eq!( <&UserId>::try_from("@te\nst:example.com").unwrap_err(), IdParseError::InvalidCharacters ); } #[test] fn missing_user_id_sigil() { assert_eq!( <&UserId>::try_from("carl:example.com").unwrap_err(), IdParseError::MissingLeadingSigil ); } #[test] fn missing_user_id_delimiter() { assert_eq!(<&UserId>::try_from("@carl").unwrap_err(), IdParseError::MissingColon); } #[test] fn invalid_user_id_host() { assert_eq!(<&UserId>::try_from("@carl:/").unwrap_err(), IdParseError::InvalidServerName); } #[test] fn invalid_user_id_port() { assert_eq!( <&UserId>::try_from("@carl:example.com:notaport").unwrap_err(), IdParseError::InvalidServerName ); } } ruma-common-0.10.5/src/identifiers/voip_id.rs000064400000000000000000000021401046102023000172310ustar 00000000000000//! VoIP identifier. use ruma_macros::IdZst; /// A VoIP identifier. /// /// VoIP IDs in Matrix are opaque strings. This type is provided simply for its semantic /// value. /// /// You can create one from a string (using `VoipId::parse()`) but the recommended way is to /// use `VoipId::new()` to generate a random one. If that function is not available for you, /// you need to activate this crate's `rand` Cargo feature. #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] pub struct VoipId(str); impl VoipId { /// Creates a random VoIP identifier. /// /// This will currently be a UUID without hyphens, but no guarantees are made about the /// structure of client secrets generated from this function. #[cfg(feature = "rand")] #[allow(clippy::new_ret_no_self)] pub fn new() -> OwnedVoipId { let id = uuid::Uuid::new_v4(); VoipId::from_borrowed(&id.simple().to_string()).to_owned() } } #[cfg(test)] mod tests { use super::VoipId; #[test] fn try_from() { <&VoipId>::try_from("this_-_a_valid_secret_1337").unwrap(); } } ruma-common-0.10.5/src/identifiers/voip_version_id.rs000064400000000000000000000136611046102023000210100ustar 00000000000000//! Matrix VoIP version identifier. use std::fmt; use js_int::UInt; use ruma_macros::DisplayAsRefStr; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, }; use crate::{IdParseError, PrivOwnedStr}; /// A Matrix VoIP version ID. /// /// A `VoipVersionId` representing VoIP version 0 can be converted or deserialized from a `UInt`, /// and can be converted or serialized back into a `UInt` as needed. /// /// Custom room versions or ones that were introduced into the specification after this code was /// written are represented by a hidden enum variant. They can be converted or deserialized from a /// string slice, and can be converted or serialized back into a string as needed. /// /// ``` /// # use ruma_common::VoipVersionId; /// assert_eq!(VoipVersionId::try_from("1").unwrap().as_ref(), "1"); /// ``` /// /// For simplicity, version 0 has a string representation, but trying to construct a `VoipVersionId` /// from a `"0"` string will not result in the `V0` variant. #[derive(Clone, Debug, PartialEq, Eq, Hash, DisplayAsRefStr)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum VoipVersionId { /// A version 0 VoIP call. V0, /// A version 1 VoIP call. #[cfg(feature = "unstable-msc2746")] V1, #[doc(hidden)] _Custom(PrivOwnedStr), } impl VoipVersionId { /// Creates a string slice from this `VoipVersionId`. pub fn as_str(&self) -> &str { match &self { Self::V0 => "0", #[cfg(feature = "unstable-msc2746")] Self::V1 => "1", Self::_Custom(PrivOwnedStr(s)) => s, } } /// Creates a byte slice from this `VoipVersionId`. pub fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } } impl From for String { fn from(id: VoipVersionId) -> Self { match id { VoipVersionId::_Custom(PrivOwnedStr(version)) => version.into(), _ => id.as_str().to_owned(), } } } impl AsRef for VoipVersionId { fn as_ref(&self) -> &str { self.as_str() } } impl<'de> Deserialize<'de> for VoipVersionId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct CallVersionVisitor; impl<'de> Visitor<'de> for CallVersionVisitor { type Value = VoipVersionId; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("0 or string") } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(value.into()) } fn visit_u64(self, value: u64) -> Result where E: de::Error, { let uint = UInt::try_from(value).map_err(de::Error::custom)?; Self::Value::try_from(uint).map_err(de::Error::custom) } } deserializer.deserialize_any(CallVersionVisitor) } } impl Serialize for VoipVersionId { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { Self::V0 => serializer.serialize_u64(0), _ => serializer.serialize_str(self.as_str()), } } } impl TryFrom for VoipVersionId { type Error = IdParseError; fn try_from(u: UInt) -> Result { ruma_identifiers_validation::voip_version_id::validate(u)?; Ok(Self::V0) } } fn from(s: T) -> VoipVersionId where T: AsRef + Into>, { match s.as_ref() { #[cfg(feature = "unstable-msc2746")] "1" => VoipVersionId::V1, _ => VoipVersionId::_Custom(PrivOwnedStr(s.into())), } } impl From<&str> for VoipVersionId { fn from(s: &str) -> Self { from(s) } } impl From for VoipVersionId { fn from(s: String) -> Self { from(s) } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use js_int::uint; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::VoipVersionId; use crate::IdParseError; #[test] fn valid_version_0() { assert_eq!(VoipVersionId::try_from(uint!(0)), Ok(VoipVersionId::V0)); } #[test] fn invalid_uint_version() { assert_matches!( VoipVersionId::try_from(uint!(1)), Err(IdParseError::InvalidVoipVersionId(_)) ); } #[test] #[cfg(feature = "unstable-msc2746")] fn valid_version_1() { assert_eq!(VoipVersionId::try_from("1"), Ok(VoipVersionId::V1)); } #[test] fn valid_custom_string_version() { let version = assert_matches!( VoipVersionId::try_from("io.ruma.2"), Ok(version) => version ); assert_eq!(version.as_ref(), "io.ruma.2"); } #[test] fn serialize_version_0() { assert_eq!(to_json_value(&VoipVersionId::V0).unwrap(), json!(0)); } #[test] fn deserialize_version_0() { assert_eq!(from_json_value::(json!(0)).unwrap(), VoipVersionId::V0); } #[test] #[cfg(feature = "unstable-msc2746")] fn serialize_version_1() { assert_eq!(to_json_value(&VoipVersionId::V1).unwrap(), json!("1")); } #[test] #[cfg(feature = "unstable-msc2746")] fn deserialize_version_1() { assert_eq!(from_json_value::(json!("1")).unwrap(), VoipVersionId::V1); } #[test] fn serialize_custom_string() { let version = VoipVersionId::try_from("io.ruma.1").unwrap(); assert_eq!(to_json_value(&version).unwrap(), json!("io.ruma.1")); } #[test] fn deserialize_custom_string() { let version = VoipVersionId::try_from("io.ruma.1").unwrap(); assert_eq!(from_json_value::(json!("io.ruma.1")).unwrap(), version); } } ruma-common-0.10.5/src/identifiers.rs000064400000000000000000000122441046102023000156060ustar 00000000000000//! Types for [Matrix](https://matrix.org/) identifiers for devices, events, keys, rooms, servers, //! users and URIs. // FIXME: Remove once lint doesn't trigger on std::convert::TryFrom in identifiers/macros.rs anymore #![allow(unused_qualifications)] use serde::de::{self, Deserializer, Unexpected}; #[doc(inline)] pub use self::{ client_secret::{ClientSecret, OwnedClientSecret}, crypto_algorithms::{ DeviceKeyAlgorithm, EventEncryptionAlgorithm, KeyDerivationAlgorithm, SigningKeyAlgorithm, }, device_id::{DeviceId, OwnedDeviceId}, device_key_id::{DeviceKeyId, OwnedDeviceKeyId}, event_id::{EventId, OwnedEventId}, key_id::{ DeviceSigningKeyId, KeyId, OwnedDeviceSigningKeyId, OwnedKeyId, OwnedServerSigningKeyId, OwnedSigningKeyId, ServerSigningKeyId, SigningKeyId, }, key_name::{KeyName, OwnedKeyName}, matrix_uri::{MatrixToUri, MatrixUri}, mxc_uri::{MxcUri, OwnedMxcUri}, room_alias_id::{OwnedRoomAliasId, RoomAliasId}, room_id::{OwnedRoomId, RoomId}, room_or_room_alias_id::{OwnedRoomOrAliasId, RoomOrAliasId}, room_version_id::RoomVersionId, server_name::{OwnedServerName, ServerName}, session_id::{OwnedSessionId, SessionId}, signatures::{DeviceSignatures, EntitySignatures, ServerSignatures, Signatures}, transaction_id::{OwnedTransactionId, TransactionId}, user_id::{OwnedUserId, UserId}, voip_id::{OwnedVoipId, VoipId}, voip_version_id::VoipVersionId, }; #[doc(inline)] pub use ruma_identifiers_validation::error::{ Error as IdParseError, MatrixIdError, MatrixToError, MatrixUriError, MxcUriError, VoipVersionIdError, }; pub mod matrix_uri; pub mod user_id; mod client_secret; mod crypto_algorithms; mod device_id; mod device_key_id; mod event_id; mod key_id; mod key_name; mod mxc_uri; mod room_alias_id; mod room_id; mod room_or_room_alias_id; mod room_version_id; mod server_name; mod session_id; mod signatures; mod transaction_id; mod voip_id; mod voip_version_id; /// Generates a random identifier localpart. #[cfg(feature = "rand")] fn generate_localpart(length: usize) -> Box { use rand::Rng as _; rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .map(char::from) .take(length) .collect::() .into_boxed_str() } /// Deserializes any type of id using the provided `TryFrom` implementation. /// /// This is a helper function to reduce the boilerplate of the `Deserialize` implementations. fn deserialize_id<'de, D, T>(deserializer: D, expected_str: &str) -> Result where D: Deserializer<'de>, T: for<'a> TryFrom<&'a str>, { crate::serde::deserialize_cow_str(deserializer).and_then(|v| { T::try_from(&v).map_err(|_| de::Error::invalid_value(Unexpected::Str(&v), &expected_str)) }) } /// Shorthand for `<&DeviceId>::from`. #[macro_export] macro_rules! device_id { ($s:expr) => { <&$crate::DeviceId as ::std::convert::From<_>>::from($s) }; } // A plain re-export shows up in rustdoc despite doc(hidden). Use a module instead. // Bug report: https://github.com/rust-lang/rust/issues/83939 #[doc(hidden)] pub mod _macros { pub use ruma_macros::{ device_key_id, event_id, mxc_uri, room_alias_id, room_id, room_version_id, server_name, server_signing_key_id, user_id, }; } /// Compile-time checked `DeviceKeyId` construction. #[macro_export] macro_rules! device_key_id { ($s:literal) => { $crate::_macros::device_key_id!($crate, $s) }; } /// Compile-time checked `EventId` construction. #[macro_export] macro_rules! event_id { ($s:literal) => { $crate::_macros::event_id!($crate, $s) }; } /// Compile-time checked `RoomAliasId` construction. #[macro_export] macro_rules! room_alias_id { ($s:literal) => { $crate::_macros::room_alias_id!($crate, $s) }; } /// Compile-time checked `RoomId` construction. #[macro_export] macro_rules! room_id { ($s:literal) => { $crate::_macros::room_id!($crate, $s) }; } /// Compile-time checked `RoomVersionId` construction. #[macro_export] macro_rules! room_version_id { ($s:literal) => { $crate::_macros::room_version_id!($crate, $s) }; } /// Compile-time checked `ServerSigningKeyId` construction. #[macro_export] macro_rules! server_signing_key_id { ($s:literal) => { $crate::_macros::server_signing_key_id!($crate, $s) }; } /// Compile-time checked `ServerName` construction. #[macro_export] macro_rules! server_name { ($s:literal) => { $crate::_macros::server_name!($crate, $s) }; } /// Compile-time checked `SessionId` construction. #[macro_export] macro_rules! session_id { ($s:literal) => {{ const SESSION_ID: &$crate::SessionId = match $crate::SessionId::_priv_const_new($s) { Ok(id) => id, Err(e) => panic!("{}", e), }; SESSION_ID }}; } /// Compile-time checked `MxcUri` construction. #[macro_export] macro_rules! mxc_uri { ($s:literal) => { $crate::_macros::mxc_uri!($crate, $s) }; } /// Compile-time checked `UserId` construction. #[macro_export] macro_rules! user_id { ($s:literal) => { $crate::_macros::user_id!($crate, $s) }; } ruma-common-0.10.5/src/lib.rs000064400000000000000000000044121046102023000140450ustar 00000000000000#![doc(html_favicon_url = "https://www.ruma.io/favicon.ico")] #![doc(html_logo_url = "https://www.ruma.io/images/logo.png")] //! Common types for the Ruma crates. #![recursion_limit = "1024"] #![warn(missing_docs)] // https://github.com/rust-lang/rust-clippy/issues/9029 #![allow(clippy::derive_partial_eq_without_eq)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #[cfg(not(all(feature = "client", feature = "server")))] compile_error!( "ruma_common's `client` and `server` Cargo features only exist as a workaround are not meant to be disabled" ); // Renamed in `Cargo.toml` so we can features with the same name as the package. // Rename them back here because the `Cargo.toml` names are ugly. #[cfg(feature = "rand")] extern crate rand_crate as rand; // Hack to allow both ruma-common itself and external crates (or tests) to use procedural macros // that expect `ruma_common` to exist in the prelude. extern crate self as ruma_common; #[cfg(feature = "api")] pub mod api; pub mod authentication; #[cfg(feature = "canonical-json")] pub mod canonical_json; pub mod directory; pub mod encryption; #[cfg(feature = "events")] pub mod events; mod identifiers; pub mod power_levels; pub mod presence; pub mod push; pub mod room; pub mod serde; pub mod thirdparty; mod time; pub mod to_device; use std::fmt; #[cfg(feature = "canonical-json")] pub use self::canonical_json::{CanonicalJsonError, CanonicalJsonObject, CanonicalJsonValue}; pub use self::{ identifiers::*, time::{MilliSecondsSinceUnixEpoch, SecondsSinceUnixEpoch}, }; // Wrapper around `Box` that cannot be used in a meaningful way outside of // this crate. Used for string enums because their `_Custom` variant can't be // truly private (only `#[doc(hidden)]`). #[doc(hidden)] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PrivOwnedStr(Box); impl fmt::Debug for PrivOwnedStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Re-exports used by macro-generated code. /// /// It is not considered part of this module's public API. #[doc(hidden)] pub mod exports { #[cfg(feature = "api")] pub use bytes; #[cfg(feature = "api")] pub use http; pub use percent_encoding; pub use ruma_macros; pub use serde; pub use serde_json; } ruma-common-0.10.5/src/power_levels.rs000064400000000000000000000025121046102023000160040ustar 00000000000000//! Common types for the [`m.room.power_levels` event][power_levels]. //! //! [power_levels]: https://spec.matrix.org/v1.2/client-server-api/#mroompower_levels use js_int::{int, Int}; use serde::{Deserialize, Serialize}; /// The power level requirements for specific notification types. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct NotificationPowerLevels { /// The level required to trigger an `@room` notification. #[serde( default = "default_power_level", deserialize_with = "crate::serde::deserialize_v1_powerlevel" )] pub room: Int, } impl NotificationPowerLevels { /// Create a new `NotificationPowerLevels` with all-default values. pub fn new() -> Self { Self { room: default_power_level() } } /// Value associated with the given `key`. pub fn get(&self, key: &str) -> Option<&Int> { match key { "room" => Some(&self.room), _ => None, } } pub(crate) fn is_default(&self) -> bool { self.room == default_power_level() } } impl Default for NotificationPowerLevels { fn default() -> Self { Self::new() } } /// Used to default power levels to 50 during deserialization. pub fn default_power_level() -> Int { int!(50) } ruma-common-0.10.5/src/presence.rs000064400000000000000000000015721046102023000151070ustar 00000000000000//! Common types for the [presence module][presence]. //! //! [presence]: https://spec.matrix.org/v1.2/client-server-api/#presence use crate::{serde::StringEnum, PrivOwnedStr}; /// A description of a user's connectivity and availability for chat. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum PresenceState { /// Disconnected from the service. Offline, /// Connected to the service. Online, /// Connected to the service but not available for chat. Unavailable, #[doc(hidden)] _Custom(PrivOwnedStr), } impl Default for PresenceState { fn default() -> Self { Self::Online } } impl Default for &'_ PresenceState { fn default() -> Self { &PresenceState::Online } } ruma-common-0.10.5/src/push/action.rs000064400000000000000000000172141046102023000155370ustar 00000000000000use std::fmt::{self, Formatter}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::value::RawValue as RawJsonValue; /// This represents the different actions that should be taken when a rule is matched, and /// controls how notifications are delivered to the client. /// /// See [the spec](https://spec.matrix.org/v1.2/client-server-api/#actions) for details. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum Action { /// Causes matching events to generate a notification. Notify, /// Prevents matching events from generating a notification. DontNotify, /// Behaves like notify but homeservers may choose to coalesce multiple events /// into a single notification. Coalesce, /// Sets an entry in the 'tweaks' dictionary sent to the push gateway. SetTweak(Tweak), } /// The `set_tweak` action. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(from = "tweak_serde::Tweak", into = "tweak_serde::Tweak")] pub enum Tweak { /// A string representing the sound to be played when this notification arrives. /// /// A value of "default" means to play a default sound. A device may choose to alert the user /// by some other means if appropriate, eg. vibration. Sound(String), /// A boolean representing whether or not this message should be highlighted in the UI. /// /// This will normally take the form of presenting the message in a different color and/or /// style. The UI might also be adjusted to draw particular attention to the room in which the /// event occurred. If a `highlight` tweak is given with no value, its value is defined to be /// `true`. If no highlight tweak is given at all then the value of `highlight` is defined to /// be `false`. Highlight(#[serde(default = "crate::serde::default_true")] bool), /// A custom tweak Custom { /// The name of the custom tweak (`set_tweak` field) name: String, /// The value of the custom tweak value: Box, }, } impl<'de> Deserialize<'de> for Action { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::{MapAccess, Visitor}; struct ActionVisitor; impl<'de> Visitor<'de> for ActionVisitor { type Value = Action; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!(formatter, "a valid action object") } /// Match a simple action type fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { match v { "notify" => Ok(Action::Notify), "dont_notify" => Ok(Action::DontNotify), "coalesce" => Ok(Action::Coalesce), s => Err(E::unknown_variant(s, &["notify", "dont_notify", "coalesce"])), } } /// Match the more complex set_tweaks action object as a key-value map fn visit_map(self, map: A) -> Result where A: MapAccess<'de>, { Tweak::deserialize(serde::de::value::MapAccessDeserializer::new(map)) .map(Action::SetTweak) } } deserializer.deserialize_any(ActionVisitor) } } impl Serialize for Action { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"), Action::DontNotify => serializer.serialize_unit_variant("Action", 1, "dont_notify"), Action::Coalesce => serializer.serialize_unit_variant("Action", 2, "coalesce"), Action::SetTweak(kind) => kind.serialize(serializer), } } } mod tweak_serde { use serde::{Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; /// Values for the `set_tweak` action. #[derive(Clone, Deserialize, Serialize)] #[serde(untagged)] pub(crate) enum Tweak { Sound(SoundTweak), Highlight(HighlightTweak), Custom { #[serde(rename = "set_tweak")] name: String, value: Box, }, } #[derive(Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "set_tweak", rename = "sound")] pub(crate) struct SoundTweak { value: String, } #[derive(Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "set_tweak", rename = "highlight")] pub(crate) struct HighlightTweak { #[serde( default = "crate::serde::default_true", skip_serializing_if = "crate::serde::is_true" )] value: bool, } impl From for Tweak { fn from(tweak: super::Tweak) -> Self { use super::Tweak::*; match tweak { Sound(value) => Self::Sound(SoundTweak { value }), Highlight(value) => Self::Highlight(HighlightTweak { value }), Custom { name, value } => Self::Custom { name, value }, } } } impl From for super::Tweak { fn from(tweak: Tweak) -> Self { use Tweak::*; match tweak { Sound(SoundTweak { value }) => Self::Sound(value), Highlight(HighlightTweak { value }) => Self::Highlight(value), Custom { name, value } => Self::Custom { name, value }, } } } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{Action, Tweak}; #[test] fn serialize_string() { assert_eq!(to_json_value(&Action::Notify).unwrap(), json!("notify")); } #[test] fn serialize_tweak_sound() { assert_eq!( to_json_value(&Action::SetTweak(Tweak::Sound("default".into()))).unwrap(), json!({ "set_tweak": "sound", "value": "default" }) ); } #[test] fn serialize_tweak_highlight() { assert_eq!( to_json_value(&Action::SetTweak(Tweak::Highlight(true))).unwrap(), json!({ "set_tweak": "highlight" }) ); assert_eq!( to_json_value(&Action::SetTweak(Tweak::Highlight(false))).unwrap(), json!({ "set_tweak": "highlight", "value": false }) ); } #[test] fn deserialize_string() { assert_matches!(from_json_value::(json!("notify")), Ok(Action::Notify)); } #[test] fn deserialize_tweak_sound() { let json_data = json!({ "set_tweak": "sound", "value": "default" }); let value = assert_matches!( from_json_value::(json_data), Ok(Action::SetTweak(Tweak::Sound(value))) => value ); assert_eq!(value, "default"); } #[test] fn deserialize_tweak_highlight() { let json_data = json!({ "set_tweak": "highlight", "value": true }); assert_matches!( from_json_value::(json_data), Ok(Action::SetTweak(Tweak::Highlight(true))) ); } #[test] fn deserialize_tweak_highlight_with_default_value() { assert_matches!( from_json_value::(json!({ "set_tweak": "highlight" })), Ok(Action::SetTweak(Tweak::Highlight(true))) ); } } ruma-common-0.10.5/src/push/condition/room_member_count_is.rs000064400000000000000000000132461046102023000224570ustar 00000000000000use std::{ fmt, ops::{Bound, RangeBounds, RangeFrom, RangeTo, RangeToInclusive}, str::FromStr, }; use js_int::UInt; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// One of `==`, `<`, `>`, `>=` or `<=`. /// /// Used by `RoomMemberCountIs`. Defaults to `==`. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[allow(clippy::exhaustive_enums)] pub enum ComparisonOperator { /// Equals Eq, /// Less than Lt, /// Greater than Gt, /// Greater or equal Ge, /// Less or equal Le, } impl Default for ComparisonOperator { fn default() -> Self { ComparisonOperator::Eq } } /// A decimal integer optionally prefixed by one of `==`, `<`, `>`, `>=` or `<=`. /// /// A prefix of `<` matches rooms where the member count is strictly less than the given /// number and so forth. If no prefix is present, this parameter defaults to `==`. /// /// Can be constructed from a number or a range: /// ``` /// use js_int::uint; /// use ruma_common::push::RoomMemberCountIs; /// /// // equivalent to `is: "3"` or `is: "==3"` /// let exact = RoomMemberCountIs::from(uint!(3)); /// /// // equivalent to `is: ">=3"` /// let greater_or_equal = RoomMemberCountIs::from(uint!(3)..); /// /// // equivalent to `is: "<3"` /// let less = RoomMemberCountIs::from(..uint!(3)); /// /// // equivalent to `is: "<=3"` /// let less_or_equal = RoomMemberCountIs::from(..=uint!(3)); /// /// // An exclusive range can be constructed with `RoomMemberCountIs::gt`: /// // (equivalent to `is: ">3"`) /// let greater = RoomMemberCountIs::gt(uint!(3)); /// ``` #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[allow(clippy::exhaustive_structs)] pub struct RoomMemberCountIs { /// One of `==`, `<`, `>`, `>=`, `<=`, or no prefix. pub prefix: ComparisonOperator, /// The number of people in the room. pub count: UInt, } impl RoomMemberCountIs { /// Creates an instance of `RoomMemberCount` equivalent to ` Self { RoomMemberCountIs { prefix: ComparisonOperator::Gt, count } } } impl From for RoomMemberCountIs { fn from(x: UInt) -> Self { RoomMemberCountIs { prefix: ComparisonOperator::Eq, count: x } } } impl From> for RoomMemberCountIs { fn from(x: RangeFrom) -> Self { RoomMemberCountIs { prefix: ComparisonOperator::Ge, count: x.start } } } impl From> for RoomMemberCountIs { fn from(x: RangeTo) -> Self { RoomMemberCountIs { prefix: ComparisonOperator::Lt, count: x.end } } } impl From> for RoomMemberCountIs { fn from(x: RangeToInclusive) -> Self { RoomMemberCountIs { prefix: ComparisonOperator::Le, count: x.end } } } impl fmt::Display for RoomMemberCountIs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ComparisonOperator as Op; let prefix = match self.prefix { Op::Eq => "", Op::Lt => "<", Op::Gt => ">", Op::Ge => ">=", Op::Le => "<=", }; write!(f, "{prefix}{}", self.count) } } impl Serialize for RoomMemberCountIs { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let s = self.to_string(); s.serialize(serializer) } } impl FromStr for RoomMemberCountIs { type Err = js_int::ParseIntError; fn from_str(s: &str) -> Result { use ComparisonOperator as Op; let (prefix, count_str) = match s { s if s.starts_with("<=") => (Op::Le, &s[2..]), s if s.starts_with('<') => (Op::Lt, &s[1..]), s if s.starts_with(">=") => (Op::Ge, &s[2..]), s if s.starts_with('>') => (Op::Gt, &s[1..]), s if s.starts_with("==") => (Op::Eq, &s[2..]), s => (Op::Eq, s), }; Ok(RoomMemberCountIs { prefix, count: UInt::from_str(count_str)? }) } } impl<'de> Deserialize<'de> for RoomMemberCountIs { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = crate::serde::deserialize_cow_str(deserializer)?; FromStr::from_str(&s).map_err(serde::de::Error::custom) } } impl RangeBounds for RoomMemberCountIs { fn start_bound(&self) -> Bound<&UInt> { use ComparisonOperator as Op; match self.prefix { Op::Eq => Bound::Included(&self.count), Op::Lt | Op::Le => Bound::Unbounded, Op::Gt => Bound::Excluded(&self.count), Op::Ge => Bound::Included(&self.count), } } fn end_bound(&self) -> Bound<&UInt> { use ComparisonOperator as Op; match self.prefix { Op::Eq => Bound::Included(&self.count), Op::Gt | Op::Ge => Bound::Unbounded, Op::Lt => Bound::Excluded(&self.count), Op::Le => Bound::Included(&self.count), } } } #[cfg(test)] mod tests { use std::ops::RangeBounds; use js_int::uint; use super::RoomMemberCountIs; #[test] fn eq_range_contains_its_own_count() { let count = uint!(2); let range = RoomMemberCountIs::from(count); assert!(range.contains(&count)); } #[test] fn ge_range_contains_large_number() { let range = RoomMemberCountIs::from(uint!(2)..); let large_number = uint!(9001); assert!(range.contains(&large_number)); } #[test] fn gt_range_does_not_contain_initial_point() { let range = RoomMemberCountIs::gt(uint!(2)); let initial_point = uint!(2); assert!(!range.contains(&initial_point)); } } ruma-common-0.10.5/src/push/condition.rs000064400000000000000000000606471046102023000162600ustar 00000000000000use std::{collections::BTreeMap, ops::RangeBounds, str::FromStr}; use js_int::{Int, UInt}; use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::{to_value as to_json_value, value::Value as JsonValue}; use tracing::{instrument, warn}; use wildmatch::WildMatch; use crate::{power_levels::NotificationPowerLevels, serde::Raw, OwnedRoomId, OwnedUserId, UserId}; mod room_member_count_is; pub use room_member_count_is::{ComparisonOperator, RoomMemberCountIs}; /// A condition that must apply for an associated push rule's action to be taken. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum PushCondition { /// A glob pattern match on a field of the event. EventMatch { /// The dot-separated field of the event to match. key: String, /// The glob-style pattern to match against. /// /// Patterns with no special glob characters should be treated as having asterisks /// prepended and appended when testing the condition. pattern: String, }, /// Matches unencrypted messages where `content.body` contains the owner's display name in that /// room. ContainsDisplayName, /// Matches the current number of members in the room. RoomMemberCount { /// The condition on the current number of members in the room. is: RoomMemberCountIs, }, /// Takes into account the current power levels in the room, ensuring the sender of the event /// has high enough power to trigger the notification. SenderNotificationPermission { /// The field in the power level event the user needs a minimum power level for. /// /// Fields must be specified under the `notifications` property in the power level event's /// `content`. key: String, }, } pub(super) fn check_event_match( event: &FlattenedJson, key: &str, pattern: &str, context: &PushConditionRoomCtx, ) -> bool { let value = match key { "room_id" => context.room_id.as_str(), _ => match event.get(key) { Some(v) => v, None => return false, }, }; value.matches_pattern(pattern, key == "content.body") } impl PushCondition { /// Check if this condition applies to the event. /// /// # Arguments /// /// * `event` - The flattened JSON representation of a room message event. /// * `context` - The context of the room at the time of the event. pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool { if event.get("sender").map_or(false, |sender| sender == context.user_id) { return false; } match self { Self::EventMatch { key, pattern } => check_event_match(event, key, pattern, context), Self::ContainsDisplayName => { let value = match event.get("content.body") { Some(v) => v, None => return false, }; value.matches_pattern(&context.user_display_name, true) } Self::RoomMemberCount { is } => is.contains(&context.member_count), Self::SenderNotificationPermission { key } => { let sender_id = match event.get("sender") { Some(v) => match <&UserId>::try_from(v) { Ok(u) => u, Err(_) => return false, }, None => return false, }; let sender_level = context .users_power_levels .get(sender_id) .unwrap_or(&context.default_power_level); match context.notification_power_levels.get(key) { Some(l) => sender_level >= l, None => false, } } } } } /// The context of the room associated to an event to be able to test all push conditions. #[derive(Clone, Debug)] #[allow(clippy::exhaustive_structs)] pub struct PushConditionRoomCtx { /// The ID of the room. pub room_id: OwnedRoomId, /// The number of members in the room. pub member_count: UInt, /// The users matrix ID. pub user_id: OwnedUserId, /// The display name of the current user in the room. pub user_display_name: String, /// The power levels of the users of the room. pub users_power_levels: BTreeMap, /// The default power level of the users of the room. pub default_power_level: Int, /// The notification power levels of the room. pub notification_power_levels: NotificationPowerLevels, } /// Additional functions for character matching. trait CharExt { /// Whether or not this char can be part of a word. fn is_word_char(&self) -> bool; } impl CharExt for char { fn is_word_char(&self) -> bool { self.is_ascii_alphanumeric() || *self == '_' } } /// Additional functions for string matching. trait StrExt { /// Get the length of the char at `index`. The byte index must correspond to /// the start of a char boundary. fn char_len(&self, index: usize) -> usize; /// Get the char at `index`. The byte index must correspond to the start of /// a char boundary. fn char_at(&self, index: usize) -> char; /// Get the index of the char that is before the char at `index`. The byte index /// must correspond to a char boundary. /// /// Returns `None` if there's no previous char. Otherwise, returns the char. fn find_prev_char(&self, index: usize) -> Option; /// Matches this string against `pattern`. /// /// The pattern can be a glob with wildcards `*` and `?`. /// /// The match is case insensitive. /// /// If `match_words` is `true`, checks that the pattern is separated from other words. fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool; /// Matches this string against `pattern`, with word boundaries. /// /// The pattern can be a glob with wildcards `*` and `?`. /// /// A word boundary is defined as the start or end of the value, or any character not in the /// sets `[A-Z]`, `[a-z]`, `[0-9]` or `_`. /// /// The match is case sensitive. fn matches_word(&self, pattern: &str) -> bool; /// Translate the wildcards in `self` to a regex syntax. /// /// `self` must only contain wildcards. fn wildcards_to_regex(&self) -> String; } impl StrExt for str { fn char_len(&self, index: usize) -> usize { let mut len = 1; while !self.is_char_boundary(index + len) { len += 1; } len } fn char_at(&self, index: usize) -> char { let end = index + self.char_len(index); let char_str = &self[index..end]; char::from_str(char_str) .unwrap_or_else(|_| panic!("Could not convert str '{}' to char", char_str)) } fn find_prev_char(&self, index: usize) -> Option { if index == 0 { return None; } let mut pos = index - 1; while !self.is_char_boundary(pos) { pos -= 1; } Some(self.char_at(pos)) } fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool { let value = &self.to_lowercase(); let pattern = &pattern.to_lowercase(); if match_words { value.matches_word(pattern) } else { WildMatch::new(pattern).matches(value) } } fn matches_word(&self, pattern: &str) -> bool { if self == pattern { return true; } if pattern.is_empty() { return false; } let has_wildcards = pattern.contains(|c| matches!(c, '?' | '*')); if has_wildcards { let mut chunks: Vec = vec![]; let mut prev_wildcard = false; let mut chunk_start = 0; for (i, c) in pattern.char_indices() { if matches!(c, '?' | '*') && !prev_wildcard { if i != 0 { chunks.push(regex::escape(&pattern[chunk_start..i])); chunk_start = i; } prev_wildcard = true; } else if prev_wildcard { let chunk = &pattern[chunk_start..i]; chunks.push(chunk.wildcards_to_regex()); chunk_start = i; prev_wildcard = false; } } let len = pattern.len(); if !prev_wildcard { chunks.push(regex::escape(&pattern[chunk_start..len])); } else if prev_wildcard { let chunk = &pattern[chunk_start..len]; chunks.push(chunk.wildcards_to_regex()); } // The word characters in ASCII compatible mode (with the `-u` flag) match the // definition in the spec: any character not in the set `[A-Za-z0-9_]`. let regex = format!(r"(?-u:^|\W|\b){}(?-u:\b|\W|$)", chunks.concat()); Regex::new(®ex).ok().filter(|re| re.is_match(self)).is_some() } else { match self.find(pattern) { Some(start) => { let end = start + pattern.len(); // Look if the match has word boundaries. let word_boundary_start = !self.char_at(start).is_word_char() || self.find_prev_char(start).map_or(true, |c| !c.is_word_char()); if word_boundary_start { let word_boundary_end = end == self.len() || !self.find_prev_char(end).unwrap().is_word_char() || !self.char_at(end).is_word_char(); if word_boundary_end { return true; } } // Find next word. let non_word_str = &self[start..]; let non_word = match non_word_str.find(|c: char| !c.is_word_char()) { Some(pos) => pos, None => return false, }; let word_str = &non_word_str[non_word..]; let word = match word_str.find(|c: char| c.is_word_char()) { Some(pos) => pos, None => return false, }; word_str[word..].matches_word(pattern) } None => false, } } } fn wildcards_to_regex(&self) -> String { // Simplify pattern to avoid performance issues: // - The glob `?**?**?` is equivalent to the glob `???*` // - The glob `???*` is equivalent to the regex `.{3,}` let question_marks = self.matches('?').count(); if self.contains('*') { format!(".{{{question_marks},}}") } else { format!(".{{{question_marks}}}") } } } /// The flattened representation of a JSON object. #[derive(Clone, Debug)] pub struct FlattenedJson { /// The internal map containing the flattened JSON as a pair path, value. map: BTreeMap, } impl FlattenedJson { /// Create a `FlattenedJson` from `Raw`. pub fn from_raw(raw: &Raw) -> Self { let mut s = Self { map: BTreeMap::new() }; s.flatten_value(to_json_value(raw).unwrap(), "".into()); s } /// Flatten and insert the `value` at `path`. #[instrument(skip(self, value))] fn flatten_value(&mut self, value: JsonValue, path: String) { match value { JsonValue::Object(fields) => { for (key, value) in fields { let path = if path.is_empty() { key } else { format!("{path}.{key}") }; self.flatten_value(value, path); } } JsonValue::String(s) => { if self.map.insert(path.clone(), s).is_some() { warn!("Duplicate path in flattened JSON: {path}"); } } JsonValue::Number(_) | JsonValue::Bool(_) | JsonValue::Array(_) | JsonValue::Null => {} } } /// Value associated with the given `path`. pub fn get(&self, path: &str) -> Option<&str> { self.map.get(path).map(|s| s.as_str()) } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use js_int::{int, uint}; use maplit::btreemap; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, }; use super::{FlattenedJson, PushCondition, PushConditionRoomCtx, RoomMemberCountIs, StrExt}; use crate::{power_levels::NotificationPowerLevels, room_id, serde::Raw, user_id}; #[test] fn serialize_event_match_condition() { let json_data = json!({ "key": "content.msgtype", "kind": "event_match", "pattern": "m.notice" }); assert_eq!( to_json_value(&PushCondition::EventMatch { key: "content.msgtype".into(), pattern: "m.notice".into(), }) .unwrap(), json_data ); } #[test] fn serialize_contains_display_name_condition() { assert_eq!( to_json_value(&PushCondition::ContainsDisplayName).unwrap(), json!({ "kind": "contains_display_name" }) ); } #[test] fn serialize_room_member_count_condition() { let json_data = json!({ "is": "2", "kind": "room_member_count" }); assert_eq!( to_json_value(&PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) }) .unwrap(), json_data ); } #[test] fn serialize_sender_notification_permission_condition() { let json_data = json!({ "key": "room", "kind": "sender_notification_permission" }); assert_eq!( json_data, to_json_value(&PushCondition::SenderNotificationPermission { key: "room".into() }) .unwrap() ); } #[test] fn deserialize_event_match_condition() { let json_data = json!({ "key": "content.msgtype", "kind": "event_match", "pattern": "m.notice" }); let (key, pattern) = assert_matches!( from_json_value::(json_data).unwrap(), PushCondition::EventMatch { key, pattern } => (key, pattern) ); assert_eq!(key, "content.msgtype"); assert_eq!(pattern, "m.notice"); } #[test] fn deserialize_contains_display_name_condition() { assert_matches!( from_json_value::(json!({ "kind": "contains_display_name" })).unwrap(), PushCondition::ContainsDisplayName ); } #[test] fn deserialize_room_member_count_condition() { let json_data = json!({ "is": "2", "kind": "room_member_count" }); let is = assert_matches!( from_json_value::(json_data).unwrap(), PushCondition::RoomMemberCount { is } => is ); assert_eq!(is, RoomMemberCountIs::from(uint!(2))); } #[test] fn deserialize_sender_notification_permission_condition() { let json_data = json!({ "key": "room", "kind": "sender_notification_permission" }); let key = assert_matches!( from_json_value::(json_data).unwrap(), PushCondition::SenderNotificationPermission { key } => key ); assert_eq!(key, "room"); } #[test] fn words_match() { assert!("foo bar".matches_word("foo")); assert!(!"Foo bar".matches_word("foo")); assert!(!"foobar".matches_word("foo")); assert!("foobar foo".matches_word("foo")); assert!(!"foobar foobar".matches_word("foo")); assert!(!"foobar bar".matches_word("bar bar")); assert!("foobar bar bar".matches_word("bar bar")); assert!(!"foobar bar barfoo".matches_word("bar bar")); assert!("conduit ⚡️".matches_word("conduit ⚡️")); assert!("conduit ⚡️".matches_word("conduit")); assert!("conduit ⚡️".matches_word("⚡️")); assert!("conduit⚡️".matches_word("conduit")); assert!("conduit⚡️".matches_word("⚡️")); assert!("⚡️conduit".matches_word("conduit")); assert!("⚡️conduit".matches_word("⚡️")); assert!("Ruma Dev👩‍💻".matches_word("Dev")); assert!("Ruma Dev👩‍💻".matches_word("👩‍💻")); assert!("Ruma Dev👩‍💻".matches_word("Dev👩‍💻")); // Regex syntax is escaped assert!(!"matrix".matches_word(r"\w*")); assert!(r"\w".matches_word(r"\w*")); assert!(!"matrix".matches_word("[a-z]*")); assert!("[a-z] and [0-9]".matches_word("[a-z]*")); assert!(!"m".matches_word("[[:alpha:]]?")); assert!("[[:alpha:]]!".matches_word("[[:alpha:]]?")); // From the spec: assert!("An example event.".matches_word("ex*ple")); assert!("exple".matches_word("ex*ple")); assert!("An exciting triple-whammy".matches_word("ex*ple")); } #[test] fn patterns_match() { // Word matching without glob assert!("foo bar".matches_pattern("foo", true)); assert!("Foo bar".matches_pattern("foo", true)); assert!(!"foobar".matches_pattern("foo", true)); assert!("".matches_pattern("", true)); assert!(!"foo".matches_pattern("", true)); assert!("foo bar".matches_pattern("foo bar", true)); assert!(" foo bar ".matches_pattern("foo bar", true)); assert!("baz foo bar baz".matches_pattern("foo bar", true)); assert!("foo baré".matches_pattern("foo bar", true)); assert!(!"bar foo".matches_pattern("foo bar", true)); assert!("foo bar".matches_pattern("foo ", true)); assert!("foo ".matches_pattern("foo ", true)); assert!("foo ".matches_pattern("foo ", true)); assert!(" foo ".matches_pattern("foo ", true)); // Word matching with glob assert!("foo bar".matches_pattern("foo*", true)); assert!("foo bar".matches_pattern("foo b?r", true)); assert!(" foo bar ".matches_pattern("foo b?r", true)); assert!("baz foo bar baz".matches_pattern("foo b?r", true)); assert!("foo baré".matches_pattern("foo b?r", true)); assert!(!"bar foo".matches_pattern("foo b?r", true)); assert!("foo bar".matches_pattern("f*o ", true)); assert!("foo ".matches_pattern("f*o ", true)); assert!("foo ".matches_pattern("f*o ", true)); assert!(" foo ".matches_pattern("f*o ", true)); // Glob matching assert!(!"foo bar".matches_pattern("foo", false)); assert!("foo".matches_pattern("foo", false)); assert!("foo".matches_pattern("foo*", false)); assert!("foobar".matches_pattern("foo*", false)); assert!("foo bar".matches_pattern("foo*", false)); assert!(!"foo".matches_pattern("foo?", false)); assert!("fooo".matches_pattern("foo?", false)); assert!("FOO".matches_pattern("foo", false)); assert!("".matches_pattern("", false)); assert!("".matches_pattern("*", false)); assert!(!"foo".matches_pattern("", false)); // From the spec: assert!("Lunch plans".matches_pattern("lunc?*", false)); assert!("LUNCH".matches_pattern("lunc?*", false)); assert!(!" lunch".matches_pattern("lunc?*", false)); assert!(!"lunc".matches_pattern("lunc?*", false)); } #[test] fn conditions_apply_to_events() { let first_sender = user_id!("@worthy_whale:server.name").to_owned(); let mut users_power_levels = BTreeMap::new(); users_power_levels.insert(first_sender, int!(25)); let context = PushConditionRoomCtx { room_id: room_id!("!room:server.name").to_owned(), member_count: uint!(3), user_id: user_id!("@gorilla:server.name").to_owned(), user_display_name: "Groovy Gorilla".into(), users_power_levels, default_power_level: int!(50), notification_power_levels: NotificationPowerLevels { room: int!(50) }, }; let first_event_raw = serde_json::from_str::>( r#"{ "sender": "@worthy_whale:server.name", "content": { "msgtype": "m.text", "body": "@room Give a warm welcome to Groovy Gorilla" } }"#, ) .unwrap(); let first_event = FlattenedJson::from_raw(&first_event_raw); let second_event_raw = serde_json::from_str::>( r#"{ "sender": "@party_bot:server.name", "content": { "msgtype": "m.notice", "body": "@room Ready to come to the party?" } }"#, ) .unwrap(); let second_event = FlattenedJson::from_raw(&second_event_raw); let correct_room = PushCondition::EventMatch { key: "room_id".into(), pattern: "!room:server.name".into(), }; let incorrect_room = PushCondition::EventMatch { key: "room_id".into(), pattern: "!incorrect:server.name".into(), }; assert!(correct_room.applies(&first_event, &context)); assert!(!incorrect_room.applies(&first_event, &context)); let keyword = PushCondition::EventMatch { key: "content.body".into(), pattern: "come".into() }; assert!(!keyword.applies(&first_event, &context)); assert!(keyword.applies(&second_event, &context)); let msgtype = PushCondition::EventMatch { key: "content.msgtype".into(), pattern: "m.notice".into() }; assert!(!msgtype.applies(&first_event, &context)); assert!(msgtype.applies(&second_event, &context)); let member_count_eq = PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(3)) }; let member_count_gt = PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)..) }; let member_count_lt = PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(..uint!(3)) }; assert!(member_count_eq.applies(&first_event, &context)); assert!(member_count_gt.applies(&first_event, &context)); assert!(!member_count_lt.applies(&first_event, &context)); let contains_display_name = PushCondition::ContainsDisplayName; assert!(contains_display_name.applies(&first_event, &context)); assert!(!contains_display_name.applies(&second_event, &context)); let sender_notification_permission = PushCondition::SenderNotificationPermission { key: "room".into() }; assert!(!sender_notification_permission.applies(&first_event, &context)); assert!(sender_notification_permission.applies(&second_event, &context)); } #[test] fn flattened_json_values() { let raw = serde_json::from_str::>( r#"{ "string": "Hello World", "number": 10, "array": [1, 2], "boolean": true, "null": null }"#, ) .unwrap(); let flattened = FlattenedJson::from_raw(&raw); assert_eq!(flattened.map, btreemap! { "string".into() => "Hello World".into() }); } #[test] fn flattened_json_nested() { let raw = serde_json::from_str::>( r#"{ "desc": "Level 0", "up": { "desc": "Level 1", "up": { "desc": "Level 2" } } }"#, ) .unwrap(); let flattened = FlattenedJson::from_raw(&raw); assert_eq!( flattened.map, btreemap! { "desc".into() => "Level 0".into(), "up.desc".into() => "Level 1".into(), "up.up.desc".into() => "Level 2".into(), }, ); } } ruma-common-0.10.5/src/push/iter.rs000064400000000000000000000172101046102023000152210ustar 00000000000000use indexmap::set::{IntoIter as IndexSetIntoIter, Iter as IndexSetIter}; use super::{ condition, Action, ConditionalPushRule, FlattenedJson, PatternedPushRule, PushConditionRoomCtx, Ruleset, SimplePushRule, }; /// The kinds of push rules that are available. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum AnyPushRule { /// Rules that override all other kinds. Override(ConditionalPushRule), /// Content-specific rules. Content(PatternedPushRule), /// Room-specific rules. Room(SimplePushRule), /// Sender-specific rules. Sender(SimplePushRule), /// Lowest priority rules. Underride(ConditionalPushRule), } impl AnyPushRule { /// Convert `AnyPushRule` to `AnyPushRuleRef`. pub fn as_ref(&self) -> AnyPushRuleRef<'_> { match self { Self::Override(o) => AnyPushRuleRef::Override(o), Self::Content(c) => AnyPushRuleRef::Content(c), Self::Room(r) => AnyPushRuleRef::Room(r), Self::Sender(s) => AnyPushRuleRef::Sender(s), Self::Underride(u) => AnyPushRuleRef::Underride(u), } } /// Get the `enabled` flag of the push rule. pub fn enabled(&self) -> bool { self.as_ref().enabled() } /// Get the `actions` of the push rule. pub fn actions(&self) -> &[Action] { self.as_ref().actions() } /// Get the `rule_id` of the push rule. pub fn rule_id(&self) -> &str { self.as_ref().rule_id() } /// Check if the push rule applies to the event. /// /// # Arguments /// /// * `event` - The flattened JSON representation of a room message event. /// * `context` - The context of the room at the time of the event. pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool { self.as_ref().applies(event, context) } } impl Extend for Ruleset { fn extend(&mut self, iter: T) where T: IntoIterator, { for rule in iter { self.add(rule); } } } /// Iterator type for `Ruleset` #[derive(Debug)] pub struct RulesetIntoIter { content: IndexSetIntoIter, override_: IndexSetIntoIter, room: IndexSetIntoIter, sender: IndexSetIntoIter, underride: IndexSetIntoIter, } impl Iterator for RulesetIntoIter { type Item = AnyPushRule; fn next(&mut self) -> Option { self.override_ .next() .map(AnyPushRule::Override) .or_else(|| self.content.next().map(AnyPushRule::Content)) .or_else(|| self.room.next().map(AnyPushRule::Room)) .or_else(|| self.sender.next().map(AnyPushRule::Sender)) .or_else(|| self.underride.next().map(AnyPushRule::Underride)) } } impl IntoIterator for Ruleset { type Item = AnyPushRule; type IntoIter = RulesetIntoIter; fn into_iter(self) -> Self::IntoIter { RulesetIntoIter { content: self.content.into_iter(), override_: self.override_.into_iter(), room: self.room.into_iter(), sender: self.sender.into_iter(), underride: self.underride.into_iter(), } } } /// Reference to any kind of push rule. #[derive(Clone, Copy, Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum AnyPushRuleRef<'a> { /// Rules that override all other kinds. Override(&'a ConditionalPushRule), /// Content-specific rules. Content(&'a PatternedPushRule), /// Room-specific rules. Room(&'a SimplePushRule), /// Sender-specific rules. Sender(&'a SimplePushRule), /// Lowest priority rules. Underride(&'a ConditionalPushRule), } impl<'a> AnyPushRuleRef<'a> { /// Convert `AnyPushRuleRef` to `AnyPushRule` by cloning the inner value. pub fn to_owned(self) -> AnyPushRule { match self { Self::Override(o) => AnyPushRule::Override(o.clone()), Self::Content(c) => AnyPushRule::Content(c.clone()), Self::Room(r) => AnyPushRule::Room(r.clone()), Self::Sender(s) => AnyPushRule::Sender(s.clone()), Self::Underride(u) => AnyPushRule::Underride(u.clone()), } } /// Get the `enabled` flag of the push rule. pub fn enabled(self) -> bool { match self { Self::Override(rule) => rule.enabled, Self::Underride(rule) => rule.enabled, Self::Content(rule) => rule.enabled, Self::Room(rule) => rule.enabled, Self::Sender(rule) => rule.enabled, } } /// Get the `actions` of the push rule. pub fn actions(self) -> &'a [Action] { match self { Self::Override(rule) => &rule.actions, Self::Underride(rule) => &rule.actions, Self::Content(rule) => &rule.actions, Self::Room(rule) => &rule.actions, Self::Sender(rule) => &rule.actions, } } /// Get the `rule_id` of the push rule. pub fn rule_id(self) -> &'a str { match self { Self::Override(rule) => &rule.rule_id, Self::Underride(rule) => &rule.rule_id, Self::Content(rule) => &rule.rule_id, Self::Room(rule) => &rule.rule_id, Self::Sender(rule) => &rule.rule_id, } } /// Check if the push rule applies to the event. /// /// # Arguments /// /// * `event` - The flattened JSON representation of a room message event. /// * `context` - The context of the room at the time of the event. pub fn applies(self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool { if event.get("sender").map_or(false, |sender| sender == context.user_id) { return false; } match self { Self::Override(rule) => rule.applies(event, context), Self::Underride(rule) => rule.applies(event, context), Self::Content(rule) => rule.applies_to("content.body", event, context), Self::Room(rule) => { rule.enabled && condition::check_event_match(event, "room_id", &rule.rule_id, context) } Self::Sender(rule) => { rule.enabled && condition::check_event_match(event, "sender", &rule.rule_id, context) } } } } /// Iterator type for `Ruleset` #[derive(Debug)] pub struct RulesetIter<'a> { content: IndexSetIter<'a, PatternedPushRule>, override_: IndexSetIter<'a, ConditionalPushRule>, room: IndexSetIter<'a, SimplePushRule>, sender: IndexSetIter<'a, SimplePushRule>, underride: IndexSetIter<'a, ConditionalPushRule>, } impl<'a> Iterator for RulesetIter<'a> { type Item = AnyPushRuleRef<'a>; fn next(&mut self) -> Option { self.override_ .next() .map(AnyPushRuleRef::Override) .or_else(|| self.content.next().map(AnyPushRuleRef::Content)) .or_else(|| self.room.next().map(AnyPushRuleRef::Room)) .or_else(|| self.sender.next().map(AnyPushRuleRef::Sender)) .or_else(|| self.underride.next().map(AnyPushRuleRef::Underride)) } } impl<'a> IntoIterator for &'a Ruleset { type Item = AnyPushRuleRef<'a>; type IntoIter = RulesetIter<'a>; fn into_iter(self) -> Self::IntoIter { RulesetIter { content: self.content.iter(), override_: self.override_.iter(), room: self.room.iter(), sender: self.sender.iter(), underride: self.underride.iter(), } } } ruma-common-0.10.5/src/push/predefined.rs000064400000000000000000000316451046102023000163730ustar 00000000000000///! Constructors for [predefined push rules]. ///! ///! [predefined push rules]: https://spec.matrix.org/v1.2/client-server-api/#predefined-rules use super::{ Action::*, ConditionalPushRule, PatternedPushRule, PushCondition::*, RoomMemberCountIs, Ruleset, Tweak, }; use crate::UserId; impl Ruleset { /// The list of all [predefined push rules]. /// /// [predefined push rules]: https://spec.matrix.org/v1.2/client-server-api/#predefined-rules /// /// # Parameters /// /// - `user_id`: the user for which to generate the default rules. Some rules depend on the /// user's ID (for instance those to send notifications when they are mentioned). pub fn server_default(user_id: &UserId) -> Self { Self { content: [PatternedPushRule::contains_user_name(user_id)].into(), override_: [ ConditionalPushRule::master(), ConditionalPushRule::suppress_notices(), ConditionalPushRule::invite_for_me(user_id), ConditionalPushRule::member_event(), ConditionalPushRule::contains_display_name(), ConditionalPushRule::tombstone(), #[cfg(feature = "unstable-msc3786")] ConditionalPushRule::server_acl(), ConditionalPushRule::roomnotif(), #[cfg(feature = "unstable-msc2677")] ConditionalPushRule::reaction(), ] .into(), underride: [ ConditionalPushRule::call(), ConditionalPushRule::encrypted_room_one_to_one(), ConditionalPushRule::room_one_to_one(), ConditionalPushRule::message(), ConditionalPushRule::encrypted(), #[cfg(feature = "unstable-msc3381")] ConditionalPushRule::poll_start_one_to_one(), #[cfg(feature = "unstable-msc3381")] ConditionalPushRule::poll_start(), #[cfg(feature = "unstable-msc3381")] ConditionalPushRule::poll_end_one_to_one(), #[cfg(feature = "unstable-msc3381")] ConditionalPushRule::poll_end(), ] .into(), ..Default::default() } } } /// Default override push rules impl ConditionalPushRule { /// Matches all events, this can be enabled to turn off all push notifications other than those /// generated by override rules set by the user. pub fn master() -> Self { Self { actions: vec![DontNotify], default: true, enabled: false, rule_id: ".m.rule.master".into(), conditions: vec![], } } /// Matches messages with a `msgtype` of `notice`. pub fn suppress_notices() -> Self { Self { actions: vec![DontNotify], default: true, enabled: true, rule_id: ".m.rule.suppress_notices".into(), conditions: vec![EventMatch { key: "content.msgtype".into(), pattern: "m.notice".into(), }], } } /// Matches any invites to a new room for this user. pub fn invite_for_me(user_id: &UserId) -> Self { Self { actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(false)), ], default: true, enabled: true, rule_id: ".m.rule.invite_for_me".into(), conditions: vec![ EventMatch { key: "type".into(), pattern: "m.room.member".into() }, EventMatch { key: "content.membership".into(), pattern: "invite".into() }, EventMatch { key: "state_key".into(), pattern: user_id.to_string() }, ], } } /// Matches any `m.room.member_event`. pub fn member_event() -> Self { Self { actions: vec![DontNotify], default: true, enabled: true, rule_id: ".m.rule.member_event".into(), conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.member".into() }], } } /// Matches any message whose content is unencrypted and contains the user's current display /// name in the room in which it was sent. pub fn contains_display_name() -> Self { Self { actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(true)), ], default: true, enabled: true, rule_id: ".m.rule.contains_display_name".into(), conditions: vec![ContainsDisplayName], } } /// Matches any state event whose type is `m.room.tombstone`. This /// is intended to notify users of a room when it is upgraded, /// similar to what an `@room` notification would accomplish. pub fn tombstone() -> Self { Self { actions: vec![Notify, SetTweak(Tweak::Highlight(true))], default: true, enabled: false, rule_id: ".m.rule.tombstone".into(), conditions: vec![ EventMatch { key: "type".into(), pattern: "m.room.tombstone".into() }, EventMatch { key: "state_key".into(), pattern: "".into() }, ], } } /// Matches any message whose content is unencrypted and contains the text `@room`, signifying /// the whole room should be notified of the event. pub fn roomnotif() -> Self { Self { actions: vec![Notify, SetTweak(Tweak::Highlight(true))], default: true, enabled: true, rule_id: ".m.rule.roomnotif".into(), conditions: vec![ EventMatch { key: "content.body".into(), pattern: "@room".into() }, SenderNotificationPermission { key: "room".into() }, ], } } /// Matches emoji reactions to a message /// MSC2677: Annotations and Reactions #[cfg(feature = "unstable-msc2677")] pub fn reaction() -> Self { Self { actions: vec![DontNotify], default: true, enabled: true, rule_id: ".m.rule.reaction".into(), conditions: vec![EventMatch { key: "type".into(), pattern: "m.reaction".into() }], } } /// Matches [room server ACLs]. /// /// [room server ACLs]: https://spec.matrix.org/v1.3/client-server-api/#server-access-control-lists-acls-for-rooms #[cfg(feature = "unstable-msc3786")] pub fn server_acl() -> Self { Self { actions: vec![], default: true, enabled: true, rule_id: ".org.matrix.msc3786.rule.room.server_acl".into(), conditions: vec![ EventMatch { key: "type".into(), pattern: "m.room.server_acl".into() }, EventMatch { key: "state_key".into(), pattern: "".into() }, ], } } } /// Default content push rules impl PatternedPushRule { /// Matches any message whose content is unencrypted and contains the local part of the user's /// Matrix ID, separated by word boundaries. pub fn contains_user_name(user_id: &UserId) -> Self { Self { rule_id: ".m.rule.contains_user_name".into(), enabled: true, default: true, pattern: user_id.localpart().into(), actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(true)), ], } } } /// Default underrides push rules impl ConditionalPushRule { /// Matches any incoming VOIP call. pub fn call() -> Self { Self { rule_id: ".m.rule.call".into(), default: true, enabled: true, conditions: vec![EventMatch { key: "type".into(), pattern: "m.call.invite".into() }], actions: vec![ Notify, SetTweak(Tweak::Sound("ring".into())), SetTweak(Tweak::Highlight(false)), ], } } /// Matches any encrypted event sent in a room with exactly two members. /// /// Unlike other push rules, this rule cannot be matched against the content of the event by /// nature of it being encrypted. This causes the rule to be an "all or nothing" match where it /// either matches all events that are encrypted (in 1:1 rooms) or none. pub fn encrypted_room_one_to_one() -> Self { Self { rule_id: ".m.rule.encrypted_room_one_to_one".into(), default: true, enabled: true, conditions: vec![ RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) }, EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() }, ], actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(false)), ], } } /// Matches any message sent in a room with exactly two members. pub fn room_one_to_one() -> Self { Self { rule_id: ".m.rule.room_one_to_one".into(), default: true, enabled: true, conditions: vec![ RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) }, EventMatch { key: "type".into(), pattern: "m.room.message".into() }, ], actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(false)), ], } } /// Matches all chat messages. pub fn message() -> Self { Self { rule_id: ".m.rule.message".into(), default: true, enabled: true, conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.message".into() }], actions: vec![Notify, SetTweak(Tweak::Highlight(false))], } } /// Matches all encrypted events. /// /// Unlike other push rules, this rule cannot be matched against the content of the event by /// nature of it being encrypted. This causes the rule to be an "all or nothing" match where it /// either matches all events that are encrypted (in group rooms) or none. pub fn encrypted() -> Self { Self { rule_id: ".m.rule.encrypted".into(), default: true, enabled: true, conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() }], actions: vec![Notify, SetTweak(Tweak::Highlight(false))], } } /// Matches a poll start event sent in a room with exactly two members. /// /// This rule should be kept in sync with `.m.rule.room_one_to_one` by the server. #[cfg(feature = "unstable-msc3381")] pub fn poll_start_one_to_one() -> Self { Self { rule_id: ".m.rule.poll_start_one_to_one".into(), default: true, enabled: true, conditions: vec![ RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) }, EventMatch { key: "type".into(), pattern: "m.poll.start".into() }, ], actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))], } } /// Matches a poll start event sent in any room. /// /// This rule should be kept in sync with `.m.rule.message` by the server. #[cfg(feature = "unstable-msc3381")] pub fn poll_start() -> Self { Self { rule_id: ".m.rule.poll_start".into(), default: true, enabled: true, conditions: vec![EventMatch { key: "type".into(), pattern: "m.poll.start".into() }], actions: vec![Notify], } } /// Matches a poll end event sent in a room with exactly two members. /// /// This rule should be kept in sync with `.m.rule.room_one_to_one` by the server. #[cfg(feature = "unstable-msc3381")] pub fn poll_end_one_to_one() -> Self { Self { rule_id: ".m.rule.poll_end_one_to_one".into(), default: true, enabled: true, conditions: vec![ RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) }, EventMatch { key: "type".into(), pattern: "m.poll.end".into() }, ], actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))], } } /// Matches a poll end event sent in any room. /// /// This rule should be kept in sync with `.m.rule.message` by the server. #[cfg(feature = "unstable-msc3381")] pub fn poll_end() -> Self { Self { rule_id: ".m.rule.poll_end".into(), default: true, enabled: true, conditions: vec![EventMatch { key: "type".into(), pattern: "m.poll.end".into() }], actions: vec![Notify], } } } ruma-common-0.10.5/src/push.rs000064400000000000000000001150341046102023000142610ustar 00000000000000//! Common types for the [push notifications module][push]. //! //! [push]: https://spec.matrix.org/v1.2/client-server-api/#push-notifications //! //! ## Understanding the types of this module //! //! Push rules are grouped in `RuleSet`s, and are grouped in five kinds (for //! more details about the different kind of rules, see the `Ruleset` documentation, //! or the specification). These five kinds are, by order of priority: //! //! - override rules //! - content rules //! - room rules //! - sender rules //! - underride rules use std::hash::{Hash, Hasher}; use indexmap::{Equivalent, IndexSet}; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-pre-spec")] use serde_json::Value as JsonValue; use tracing::instrument; use crate::{ serde::{Raw, StringEnum}, PrivOwnedStr, }; mod action; mod condition; mod iter; mod predefined; pub use self::{ action::{Action, Tweak}, condition::{ ComparisonOperator, FlattenedJson, PushCondition, PushConditionRoomCtx, RoomMemberCountIs, }, iter::{AnyPushRule, AnyPushRuleRef, RulesetIntoIter, RulesetIter}, }; /// A push ruleset scopes a set of rules according to some criteria. /// /// For example, some rules may only be applied for messages from a particular sender, a particular /// room, or by default. The push ruleset contains the entire set of scopes and rules. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Ruleset { /// These rules configure behavior for (unencrypted) messages that match certain patterns. pub content: IndexSet, /// These user-configured rules are given the highest priority. /// /// This field is named `override_` instead of `override` because the latter is a reserved /// keyword in Rust. #[serde(rename = "override")] pub override_: IndexSet, /// These rules change the behavior of all messages for a given room. pub room: IndexSet, /// These rules configure notification behavior for messages from a specific Matrix user ID. pub sender: IndexSet, /// These rules are identical to override rules, but have a lower priority than `content`, /// `room` and `sender` rules. pub underride: IndexSet, } impl Ruleset { /// Creates an empty `Ruleset`. pub fn new() -> Self { Default::default() } /// Creates a borrowing iterator over all push rules in this `Ruleset`. /// /// For an owning iterator, use `.into_iter()`. pub fn iter(&self) -> RulesetIter<'_> { self.into_iter() } /// Adds a rule to the rule set. /// /// Returns `true` if the new rule was correctly added, and `false` /// if a rule with the same `rule_id` is already present for this kind /// of rule. pub fn add(&mut self, rule: AnyPushRule) -> bool { match rule { AnyPushRule::Override(r) => self.override_.insert(r), AnyPushRule::Underride(r) => self.underride.insert(r), AnyPushRule::Content(r) => self.content.insert(r), AnyPushRule::Room(r) => self.room.insert(r), AnyPushRule::Sender(r) => self.sender.insert(r), } } /// Get the first push rule that applies to this event, if any. /// /// # Arguments /// /// * `event` - The raw JSON of a room message event. /// * `context` - The context of the message and room at the time of the event. #[instrument(skip_all, fields(context.room_id = %context.room_id))] pub fn get_match( &self, event: &Raw, context: &PushConditionRoomCtx, ) -> Option> { let event = FlattenedJson::from_raw(event); if event.get("sender").map_or(false, |sender| sender == context.user_id) { // no need to look at the rules if the event was by the user themselves None } else { self.iter().find(|rule| rule.applies(&event, context)) } } /// Get the push actions that apply to this event. /// /// Returns an empty slice if no push rule applies. /// /// # Arguments /// /// * `event` - The raw JSON of a room message event. /// * `context` - The context of the message and room at the time of the event. #[instrument(skip_all, fields(context.room_id = %context.room_id))] pub fn get_actions(&self, event: &Raw, context: &PushConditionRoomCtx) -> &[Action] { self.get_match(event, context).map(|rule| rule.actions()).unwrap_or(&[]) } } /// A push rule is a single rule that states under what conditions an event should be passed onto a /// push gateway and how the notification should be presented. /// /// These rules are stored on the user's homeserver. They are manually configured by the user, who /// can create and view them via the Client/Server API. /// /// To create an instance of this type, first create a `SimplePushRuleInit` and convert it via /// `SimplePushRule::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SimplePushRule { /// Actions to determine if and how a notification is delivered for events matching this rule. pub actions: Vec, /// Whether this is a default rule, or has been set explicitly. pub default: bool, /// Whether the push rule is enabled or not. pub enabled: bool, /// The ID of this rule. pub rule_id: String, } /// Initial set of fields of `SimplePushRule`. /// /// This struct will not be updated even if additional fields are added to `SimplePushRule` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct SimplePushRuleInit { /// Actions to determine if and how a notification is delivered for events matching this rule. pub actions: Vec, /// Whether this is a default rule, or has been set explicitly. pub default: bool, /// Whether the push rule is enabled or not. pub enabled: bool, /// The ID of this rule. pub rule_id: String, } impl From for SimplePushRule { fn from(init: SimplePushRuleInit) -> Self { let SimplePushRuleInit { actions, default, enabled, rule_id } = init; Self { actions, default, enabled, rule_id } } } // The following trait are needed to be able to make // an IndexSet of the type impl Hash for SimplePushRule { fn hash(&self, state: &mut H) { self.rule_id.hash(state); } } impl PartialEq for SimplePushRule { fn eq(&self, other: &Self) -> bool { self.rule_id == other.rule_id } } impl Eq for SimplePushRule {} impl Equivalent for str { fn equivalent(&self, key: &SimplePushRule) -> bool { self == key.rule_id } } /// Like `SimplePushRule`, but with an additional `conditions` field. /// /// Only applicable to underride and override rules. /// /// To create an instance of this type, first create a `ConditionalPushRuleInit` and convert it via /// `ConditionalPushRule::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ConditionalPushRule { /// Actions to determine if and how a notification is delivered for events matching this rule. pub actions: Vec, /// Whether this is a default rule, or has been set explicitly. pub default: bool, /// Whether the push rule is enabled or not. pub enabled: bool, /// The ID of this rule. pub rule_id: String, /// The conditions that must hold true for an event in order for a rule to be applied to an /// event. /// /// A rule with no conditions always matches. pub conditions: Vec, } impl ConditionalPushRule { /// Check if the push rule applies to the event. /// /// # Arguments /// /// * `event` - The flattened JSON representation of a room message event. /// * `context` - The context of the room at the time of the event. pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool { self.enabled && self.conditions.iter().all(|cond| cond.applies(event, context)) } } /// Initial set of fields of `ConditionalPushRule`. /// /// This struct will not be updated even if additional fields are added to `ConditionalPushRule` in /// a new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct ConditionalPushRuleInit { /// Actions to determine if and how a notification is delivered for events matching this rule. pub actions: Vec, /// Whether this is a default rule, or has been set explicitly. pub default: bool, /// Whether the push rule is enabled or not. pub enabled: bool, /// The ID of this rule. pub rule_id: String, /// The conditions that must hold true for an event in order for a rule to be applied to an /// event. /// /// A rule with no conditions always matches. pub conditions: Vec, } impl From for ConditionalPushRule { fn from(init: ConditionalPushRuleInit) -> Self { let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init; Self { actions, default, enabled, rule_id, conditions } } } // The following trait are needed to be able to make // an IndexSet of the type impl Hash for ConditionalPushRule { fn hash(&self, state: &mut H) { self.rule_id.hash(state); } } impl PartialEq for ConditionalPushRule { fn eq(&self, other: &Self) -> bool { self.rule_id == other.rule_id } } impl Eq for ConditionalPushRule {} impl Equivalent for str { fn equivalent(&self, key: &ConditionalPushRule) -> bool { self == key.rule_id } } /// Like `SimplePushRule`, but with an additional `pattern` field. /// /// Only applicable to content rules. /// /// To create an instance of this type, first create a `PatternedPushRuleInit` and convert it via /// `PatternedPushRule::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PatternedPushRule { /// Actions to determine if and how a notification is delivered for events matching this rule. pub actions: Vec, /// Whether this is a default rule, or has been set explicitly. pub default: bool, /// Whether the push rule is enabled or not. pub enabled: bool, /// The ID of this rule. pub rule_id: String, /// The glob-style pattern to match against. pub pattern: String, } impl PatternedPushRule { /// Check if the push rule applies to the event. /// /// # Arguments /// /// * `event` - The flattened JSON representation of a room message event. /// * `context` - The context of the room at the time of the event. pub fn applies_to( &self, key: &str, event: &FlattenedJson, context: &PushConditionRoomCtx, ) -> bool { if event.get("sender").map_or(false, |sender| sender == context.user_id) { return false; } self.enabled && condition::check_event_match(event, key, &self.pattern, context) } } /// Initial set of fields of `PatterenedPushRule`. /// /// This struct will not be updated even if additional fields are added to `PatterenedPushRule` in a /// new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct PatternedPushRuleInit { /// Actions to determine if and how a notification is delivered for events matching this rule. pub actions: Vec, /// Whether this is a default rule, or has been set explicitly. pub default: bool, /// Whether the push rule is enabled or not. pub enabled: bool, /// The ID of this rule. pub rule_id: String, /// The glob-style pattern to match against. pub pattern: String, } impl From for PatternedPushRule { fn from(init: PatternedPushRuleInit) -> Self { let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init; Self { actions, default, enabled, rule_id, pattern } } } // The following trait are needed to be able to make // an IndexSet of the type impl Hash for PatternedPushRule { fn hash(&self, state: &mut H) { self.rule_id.hash(state); } } impl PartialEq for PatternedPushRule { fn eq(&self, other: &Self) -> bool { self.rule_id == other.rule_id } } impl Eq for PatternedPushRule {} impl Equivalent for str { fn equivalent(&self, key: &PatternedPushRule) -> bool { self == key.rule_id } } /// Information for the pusher implementation itself. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PusherData { /// The URL to use to send notifications to. /// /// Required if the pusher's kind is http. #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, /// The format to use when sending notifications to the Push Gateway. #[serde(skip_serializing_if = "Option::is_none")] pub format: Option, /// iOS (+ macOS?) specific default payload that will be sent to apple push notification /// service. /// /// For more information, see [Sygnal docs][sygnal]. /// /// [sygnal]: https://github.com/matrix-org/sygnal/blob/main/docs/applications.md#ios-applications-beware // Not specified, issue: https://github.com/matrix-org/matrix-spec/issues/921 #[cfg(feature = "unstable-pre-spec")] #[serde(default, skip_serializing_if = "JsonValue::is_null")] pub default_payload: JsonValue, } impl PusherData { /// Creates an empty `PusherData`. pub fn new() -> Self { Default::default() } /// Returns `true` if all fields are `None`. pub fn is_empty(&self) -> bool { #[cfg(not(feature = "unstable-pre-spec"))] { self.url.is_none() && self.format.is_none() } #[cfg(feature = "unstable-pre-spec")] { self.url.is_none() && self.format.is_none() && self.default_payload.is_null() } } } /// A special format that the homeserver should use when sending notifications to a Push Gateway. /// Currently, only "event_id_only" is supported as of [Push Gateway API r0.1.1][spec]. /// /// [spec]: https://spec.matrix.org/v1.2/push-gateway-api/#homeserver-behaviour #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] #[non_exhaustive] pub enum PushFormat { /// Require the homeserver to only send a reduced set of fields in the push. EventIdOnly, #[doc(hidden)] _Custom(PrivOwnedStr), } #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use js_int::{int, uint}; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, value::RawValue as RawJsonValue, Value as JsonValue, }; use super::{ action::{Action, Tweak}, condition::{PushCondition, PushConditionRoomCtx, RoomMemberCountIs}, AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule, }; use crate::{power_levels::NotificationPowerLevels, room_id, serde::Raw, user_id}; fn example_ruleset() -> Ruleset { let mut set = Ruleset::new(); set.add(AnyPushRule::Override(ConditionalPushRule { conditions: vec![PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into(), }], actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))], rule_id: ".m.rule.call".into(), enabled: true, default: true, })); set } #[test] fn cannot_add_same_rule_id() { let mut set = example_ruleset(); let added = set.add(AnyPushRule::Override(ConditionalPushRule { conditions: vec![], actions: vec![], rule_id: ".m.rule.call".into(), enabled: true, default: true, })); assert!(!added); } #[test] fn can_add_same_rule_id_different_kind() { let mut set = example_ruleset(); let added = set.add(AnyPushRule::Underride(ConditionalPushRule { conditions: vec![], actions: vec![], rule_id: ".m.rule.call".into(), enabled: true, default: true, })); assert!(added); } #[test] fn get_by_rule_id() { let set = example_ruleset(); let rule = set.override_.get(".m.rule.call"); assert!(rule.is_some()); assert_eq!(rule.unwrap().rule_id, ".m.rule.call"); let rule = set.override_.get(".m.rule.doesntexist"); assert_matches!(rule, None); } #[test] fn iter() { let mut set = example_ruleset(); let added = set.add(AnyPushRule::Override(ConditionalPushRule { conditions: vec![PushCondition::EventMatch { key: "room_id".into(), pattern: "!roomid:matrix.org".into(), }], actions: vec![Action::DontNotify], rule_id: "!roomid:matrix.org".into(), enabled: true, default: false, })); assert!(added); let added = set.add(AnyPushRule::Override(ConditionalPushRule { conditions: vec![], actions: vec![], rule_id: ".m.rule.suppress_notices".into(), enabled: false, default: true, })); assert!(added); let mut iter = set.into_iter(); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, ".m.rule.call"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, "!roomid:matrix.org"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, ".m.rule.suppress_notices"); assert_matches!(iter.next(), None); } #[test] fn serialize_conditional_push_rule() { let rule = ConditionalPushRule { actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))], default: true, enabled: true, rule_id: ".m.rule.call".into(), conditions: vec![ PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into() }, PushCondition::ContainsDisplayName, PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) }, PushCondition::SenderNotificationPermission { key: "room".into() }, ], }; let rule_value: JsonValue = to_json_value(rule).unwrap(); assert_eq!( rule_value, json!({ "conditions": [ { "kind": "event_match", "key": "type", "pattern": "m.call.invite" }, { "kind": "contains_display_name" }, { "kind": "room_member_count", "is": ">2" }, { "kind": "sender_notification_permission", "key": "room" } ], "actions": [ "notify", { "set_tweak": "highlight" } ], "rule_id": ".m.rule.call", "default": true, "enabled": true }) ); } #[test] fn serialize_simple_push_rule() { let rule = SimplePushRule { actions: vec![Action::DontNotify], default: false, enabled: false, rule_id: "!roomid:server.name".into(), }; let rule_value: JsonValue = to_json_value(rule).unwrap(); assert_eq!( rule_value, json!({ "actions": [ "dont_notify" ], "rule_id": "!roomid:server.name", "default": false, "enabled": false }) ); } #[test] fn serialize_patterned_push_rule() { let rule = PatternedPushRule { actions: vec![ Action::Notify, Action::SetTweak(Tweak::Sound("default".into())), Action::SetTweak(Tweak::Custom { name: "dance".into(), value: RawJsonValue::from_string("true".into()).unwrap(), }), ], default: true, enabled: true, pattern: "user_id".into(), rule_id: ".m.rule.contains_user_name".into(), }; let rule_value: JsonValue = to_json_value(rule).unwrap(); assert_eq!( rule_value, json!({ "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "dance", "value": true } ], "pattern": "user_id", "rule_id": ".m.rule.contains_user_name", "default": true, "enabled": true }) ); } #[test] fn serialize_ruleset() { let mut set = example_ruleset(); set.add(AnyPushRule::Override(ConditionalPushRule { conditions: vec![ PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) }, PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() }, ], actions: vec![ Action::Notify, Action::SetTweak(Tweak::Sound("default".into())), Action::SetTweak(Tweak::Highlight(false)), ], rule_id: ".m.rule.room_one_to_one".into(), enabled: true, default: true, })); set.add(AnyPushRule::Content(PatternedPushRule { actions: vec![ Action::Notify, Action::SetTweak(Tweak::Sound("default".into())), Action::SetTweak(Tweak::Highlight(true)), ], rule_id: ".m.rule.contains_user_name".into(), pattern: "user_id".into(), enabled: true, default: true, })); let set_value: JsonValue = to_json_value(set).unwrap(); assert_eq!( set_value, json!({ "override": [ { "actions": [ "notify", { "set_tweak": "highlight", }, ], "conditions": [ { "kind": "event_match", "key": "type", "pattern": "m.call.invite" }, ], "rule_id": ".m.rule.call", "default": true, "enabled": true, }, { "conditions": [ { "kind": "room_member_count", "is": "2" }, { "kind": "event_match", "key": "type", "pattern": "m.room.message" } ], "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false } ], "rule_id": ".m.rule.room_one_to_one", "default": true, "enabled": true }, ], "room": [], "content": [ { "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" } ], "pattern": "user_id", "rule_id": ".m.rule.contains_user_name", "default": true, "enabled": true } ], "sender": [], "underride": [], }) ); } #[test] fn deserialize_patterned_push_rule() { let rule = from_json_value::(json!({ "actions": [ "notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": true } ], "pattern": "user_id", "rule_id": ".m.rule.contains_user_name", "default": true, "enabled": true })) .unwrap(); assert!(rule.default); assert!(rule.enabled); assert_eq!(rule.pattern, "user_id"); assert_eq!(rule.rule_id, ".m.rule.contains_user_name"); let mut iter = rule.actions.iter(); assert_matches!(iter.next(), Some(Action::Notify)); let sound = assert_matches!( iter.next(), Some(Action::SetTweak(Tweak::Sound(sound))) => sound ); assert_eq!(sound, "default"); assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Highlight(true)))); assert_matches!(iter.next(), None); } #[test] fn deserialize_ruleset() { let set: Ruleset = from_json_value(json!({ "override": [ { "actions": [], "conditions": [], "rule_id": "!roomid:server.name", "default": false, "enabled": true }, { "actions": [], "conditions": [], "rule_id": ".m.rule.call", "default": true, "enabled": true }, ], "underride": [ { "actions": [], "conditions": [], "rule_id": ".m.rule.room_one_to_one", "default": true, "enabled": true }, ], "room": [ { "actions": [], "rule_id": "!roomid:server.name", "default": false, "enabled": false } ], "sender": [], "content": [ { "actions": [], "pattern": "user_id", "rule_id": ".m.rule.contains_user_name", "default": true, "enabled": true }, { "actions": [], "pattern": "ruma", "rule_id": "ruma", "default": false, "enabled": true } ] })) .unwrap(); let mut iter = set.into_iter(); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, "!roomid:server.name"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, ".m.rule.call"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, ".m.rule.contains_user_name"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, "ruma"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Room(SimplePushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, "!roomid:server.name"); let rule_opt = iter.next(); assert!(rule_opt.is_some()); let rule_id = assert_matches!( rule_opt.unwrap(), AnyPushRule::Underride(ConditionalPushRule { rule_id, .. }) => rule_id ); assert_eq!(rule_id, ".m.rule.room_one_to_one"); assert_matches!(iter.next(), None); } #[test] fn default_ruleset_applies() { let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name")); let context_one_to_one = &PushConditionRoomCtx { room_id: room_id!("!dm:server.name").to_owned(), member_count: uint!(2), user_id: user_id!("@jj:server.name").to_owned(), user_display_name: "Jolly Jumper".into(), users_power_levels: BTreeMap::new(), default_power_level: int!(50), notification_power_levels: NotificationPowerLevels { room: int!(50) }, }; let context_public_room = &PushConditionRoomCtx { room_id: room_id!("!far_west:server.name").to_owned(), member_count: uint!(100), user_id: user_id!("@jj:server.name").to_owned(), user_display_name: "Jolly Jumper".into(), users_power_levels: BTreeMap::new(), default_power_level: int!(50), notification_power_levels: NotificationPowerLevels { room: int!(50) }, }; let message = serde_json::from_str::>( r#"{ "type": "m.room.message" }"#, ) .unwrap(); assert_matches!( set.get_actions(&message, context_one_to_one), [ Action::Notify, Action::SetTweak(Tweak::Sound(_)), Action::SetTweak(Tweak::Highlight(false)) ] ); assert_matches!( set.get_actions(&message, context_public_room), [Action::Notify, Action::SetTweak(Tweak::Highlight(false))] ); let user_name = serde_json::from_str::>( r#"{ "type": "m.room.message", "content": { "body": "Hi jolly_jumper!" } }"#, ) .unwrap(); assert_matches!( set.get_actions(&user_name, context_one_to_one), [ Action::Notify, Action::SetTweak(Tweak::Sound(_)), Action::SetTweak(Tweak::Highlight(true)), ] ); assert_matches!( set.get_actions(&user_name, context_public_room), [ Action::Notify, Action::SetTweak(Tweak::Sound(_)), Action::SetTweak(Tweak::Highlight(true)), ] ); let notice = serde_json::from_str::>( r#"{ "type": "m.room.message", "content": { "msgtype": "m.notice" } }"#, ) .unwrap(); assert_matches!(set.get_actions(¬ice, context_one_to_one), [Action::DontNotify]); let at_room = serde_json::from_str::>( r#"{ "type": "m.room.message", "sender": "@rantanplan:server.name", "content": { "body": "@room Attention please!", "msgtype": "m.text" } }"#, ) .unwrap(); assert_matches!( set.get_actions(&at_room, context_public_room), [Action::Notify, Action::SetTweak(Tweak::Highlight(true)),] ); let empty = serde_json::from_str::>(r#"{}"#).unwrap(); assert_matches!(set.get_actions(&empty, context_one_to_one), []); } #[test] fn custom_ruleset_applies() { let context_one_to_one = &PushConditionRoomCtx { room_id: room_id!("!dm:server.name").to_owned(), member_count: uint!(2), user_id: user_id!("@jj:server.name").to_owned(), user_display_name: "Jolly Jumper".into(), users_power_levels: BTreeMap::new(), default_power_level: int!(50), notification_power_levels: NotificationPowerLevels { room: int!(50) }, }; let message = serde_json::from_str::>( r#"{ "sender": "@rantanplan:server.name", "type": "m.room.message", "content": { "msgtype": "m.text", "body": "Great joke!" } }"#, ) .unwrap(); let mut set = Ruleset::new(); let disabled = AnyPushRule::Underride(ConditionalPushRule { actions: vec![Action::Notify], default: false, enabled: false, rule_id: "disabled".into(), conditions: vec![PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)), }], }); set.add(disabled); let test_set = set.clone(); assert_matches!(test_set.get_actions(&message, context_one_to_one), []); let no_conditions = AnyPushRule::Underride(ConditionalPushRule { actions: vec![Action::SetTweak(Tweak::Highlight(true))], default: false, enabled: true, rule_id: "no.conditions".into(), conditions: vec![], }); set.add(no_conditions); let test_set = set.clone(); assert_matches!( test_set.get_actions(&message, context_one_to_one), [Action::SetTweak(Tweak::Highlight(true))] ); let sender = AnyPushRule::Sender(SimplePushRule { actions: vec![Action::Notify], default: false, enabled: true, rule_id: "@rantanplan:server.name".into(), }); set.add(sender); let test_set = set.clone(); assert_matches!(test_set.get_actions(&message, context_one_to_one), [Action::Notify]); let room = AnyPushRule::Room(SimplePushRule { actions: vec![Action::DontNotify], default: false, enabled: true, rule_id: "!dm:server.name".into(), }); set.add(room); let test_set = set.clone(); assert_matches!(test_set.get_actions(&message, context_one_to_one), [Action::DontNotify]); let content = AnyPushRule::Content(PatternedPushRule { actions: vec![Action::SetTweak(Tweak::Sound("content".into()))], default: false, enabled: true, rule_id: "content".into(), pattern: "joke".into(), }); set.add(content); let test_set = set.clone(); assert_matches!( test_set.get_actions(&message, context_one_to_one), [Action::SetTweak(Tweak::Sound(sound))] if sound == "content" ); let three_conditions = AnyPushRule::Override(ConditionalPushRule { actions: vec![Action::SetTweak(Tweak::Sound("three".into()))], default: false, enabled: true, rule_id: "three.conditions".into(), conditions: vec![ PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) }, PushCondition::ContainsDisplayName, PushCondition::EventMatch { key: "room_id".into(), pattern: "!dm:server.name".into(), }, ], }); set.add(three_conditions); let sound = assert_matches!( set.get_actions(&message, context_one_to_one), [Action::SetTweak(Tweak::Sound(sound))] => sound ); assert_eq!(sound, "content"); let new_message = serde_json::from_str::>( r#"{ "sender": "@rantanplan:server.name", "type": "m.room.message", "content": { "msgtype": "m.text", "body": "Tell me another one, Jolly Jumper!" } }"#, ) .unwrap(); let sound = assert_matches!( set.get_actions(&new_message, context_one_to_one), [Action::SetTweak(Tweak::Sound(sound))] => sound ); assert_eq!(sound, "three"); } } ruma-common-0.10.5/src/room.rs000064400000000000000000000007201046102023000142510ustar 00000000000000//! Common types for rooms. use crate::{serde::StringEnum, PrivOwnedStr}; /// An enum of possible room types. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum RoomType { /// Defines the room as a space. #[ruma_enum(rename = "m.space")] Space, /// Defines the room as a custom type. #[doc(hidden)] _Custom(PrivOwnedStr), } ruma-common-0.10.5/src/serde/base64.rs000064400000000000000000000102251046102023000154640ustar 00000000000000//!Transparent base64 encoding / decoding as part of (de)serialization. use std::{fmt, marker::PhantomData}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; /// A wrapper around `B` (usually `Vec`) that (de)serializes from / to a base64 string. /// /// The base64 character set (and miscellaneous other encoding / decoding options) can be customized /// through the generic parameter `C`. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Base64> { bytes: B, // Invariant PhantomData, Send + Sync _phantom_conf: PhantomData C>, } /// Config used for the [`Base64`] type. pub trait Base64Config { /// The config as a constant. /// /// Opaque so our interface is not tied to the base64 crate version. #[doc(hidden)] const CONF: Conf; } #[doc(hidden)] pub struct Conf(base64::Config); /// Standard base64 character set without padding. /// /// Allows trailing bits in decoding for maximum compatibility. #[non_exhaustive] // Easier than implementing these all for Base64 manually to avoid the `C: Trait` bounds. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Standard; impl Base64Config for Standard { // See https://github.com/matrix-org/matrix-spec/issues/838 const CONF: Conf = Conf(base64::STANDARD_NO_PAD.decode_allow_trailing_bits(true)); } /// Url-safe base64 character set without padding. /// /// Allows trailing bits in decoding for maximum compatibility. #[non_exhaustive] // Easier than implementing these all for Base64 manually to avoid the `C: Trait` bounds. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UrlSafe; impl Base64Config for UrlSafe { const CONF: Conf = Conf(base64::URL_SAFE_NO_PAD.decode_allow_trailing_bits(true)); } impl> Base64 { /// Create a `Base64` instance from raw bytes, to be base64-encoded in serialialization. pub fn new(bytes: B) -> Self { Self { bytes, _phantom_conf: PhantomData } } /// Get a reference to the raw bytes held by this `Base64` instance. pub fn as_bytes(&self) -> &[u8] { self.bytes.as_ref() } /// Encode the bytes contained in this `Base64` instance to unpadded base64. pub fn encode(&self) -> String { base64::encode_config(&self.bytes, C::CONF.0) } } impl Base64 { /// Get the raw bytes held by this `Base64` instance. pub fn into_inner(self) -> B { self.bytes } } impl Base64 { /// Create a `Base64` instance containing an empty `Vec`. pub fn empty() -> Self { Self::new(Vec::new()) } /// Parse some base64-encoded data to create a `Base64` instance. pub fn parse(encoded: impl AsRef<[u8]>) -> Result { base64::decode_config(encoded, C::CONF.0).map(Self::new).map_err(Base64DecodeError) } } impl> fmt::Debug for Base64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.encode().fmt(f) } } impl> fmt::Display for Base64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.encode().fmt(f) } } impl<'de, C: Base64Config> Deserialize<'de> for Base64 { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let encoded = super::deserialize_cow_str(deserializer)?; Self::parse(&*encoded).map_err(de::Error::custom) } } impl> Serialize for Base64 { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.encode()) } } /// An error that occurred while decoding a base64 string. #[derive(Clone)] pub struct Base64DecodeError(base64::DecodeError); impl fmt::Debug for Base64DecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl fmt::Display for Base64DecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl std::error::Error for Base64DecodeError {} ruma-common-0.10.5/src/serde/buf.rs000064400000000000000000000007341046102023000151600ustar 00000000000000use bytes::BufMut; use serde::Serialize; /// Converts a byte slice to a buffer by copying. pub fn slice_to_buf(s: &[u8]) -> B { let mut buf = B::default(); buf.put_slice(s); buf } /// Creates a buffer and writes a serializable value to it. pub fn json_to_buf(val: &T) -> serde_json::Result { let mut buf = B::default().writer(); serde_json::to_writer(&mut buf, val)?; Ok(buf.into_inner()) } ruma-common-0.10.5/src/serde/can_be_empty.rs000064400000000000000000000006501046102023000170260ustar 00000000000000//! Helpers for emptiness checks in `#[serde(skip_serializing_if)]`. /// Trait for types that have an "empty" state. /// /// If `Default` is implemented for `Self`, `Self::default().is_empty()` should always be `true`. pub trait CanBeEmpty { /// Check whether `self` is empty. fn is_empty(&self) -> bool; } /// Check whether a value is empty. pub fn is_empty(val: &T) -> bool { val.is_empty() } ruma-common-0.10.5/src/serde/cow.rs000064400000000000000000000041211046102023000151660ustar 00000000000000use std::{borrow::Cow, str}; use serde::de::{self, Deserializer, Unexpected, Visitor}; /// Deserialize a `Cow<'de, str>`. /// /// Different from serde's implementation of `Deserialize` for `Cow` since it borrows from the /// input when possible. /// /// This will become unnecessary if Rust gains lifetime specialization at some point; see /// . pub fn deserialize_cow_str<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_string(CowStrVisitor) } struct CowStrVisitor; impl<'de> Visitor<'de> for CowStrVisitor { type Value = Cow<'de, str>; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a string") } fn visit_borrowed_str(self, v: &'de str) -> Result where E: de::Error, { Ok(Cow::Borrowed(v)) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where E: de::Error, { match str::from_utf8(v) { Ok(s) => Ok(Cow::Borrowed(s)), Err(_) => Err(de::Error::invalid_value(Unexpected::Bytes(v), &self)), } } fn visit_str(self, v: &str) -> Result where E: de::Error, { Ok(Cow::Owned(v.to_owned())) } fn visit_string(self, v: String) -> Result where E: de::Error, { Ok(Cow::Owned(v)) } fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error, { match str::from_utf8(v) { Ok(s) => Ok(Cow::Owned(s.to_owned())), Err(_) => Err(de::Error::invalid_value(Unexpected::Bytes(v), &self)), } } fn visit_byte_buf(self, v: Vec) -> Result where E: de::Error, { match String::from_utf8(v) { Ok(s) => Ok(Cow::Owned(s)), Err(e) => Err(de::Error::invalid_value(Unexpected::Bytes(&e.into_bytes()), &self)), } } } ruma-common-0.10.5/src/serde/duration/opt_ms.rs000064400000000000000000000052261046102023000175330ustar 00000000000000//! De-/serialization functions for `Option` objects represented as //! milliseconds. //! //! Delegates to `js_int::UInt` to ensure integer size is within bounds. use std::time::Duration; use js_int::UInt; use serde::{ de::{Deserialize, Deserializer}, ser::{Error, Serialize, Serializer}, }; /// Serialize an Option. /// /// Will fail if integer is greater than the maximum integer that can be /// unambiguously represented by an f64. pub fn serialize(opt_duration: &Option, serializer: S) -> Result where S: Serializer, { match opt_duration { Some(duration) => match UInt::try_from(duration.as_millis()) { Ok(uint) => uint.serialize(serializer), Err(err) => Err(S::Error::custom(err)), }, None => serializer.serialize_none(), } } /// Deserializes an Option. /// /// Will fail if integer is greater than the maximum integer that can be /// unambiguously represented by an f64. pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { Ok(Option::::deserialize(deserializer)? .map(|millis| Duration::from_millis(millis.into()))) } #[cfg(test)] mod tests { use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] struct DurationTest { #[serde(with = "super", default, skip_serializing_if = "Option::is_none")] timeout: Option, } #[test] fn deserialize_some() { let json = json!({ "timeout": 3000 }); assert_eq!( serde_json::from_value::(json).unwrap(), DurationTest { timeout: Some(Duration::from_millis(3000)) }, ); } #[test] fn deserialize_none_by_absence() { let json = json!({}); assert_eq!( serde_json::from_value::(json).unwrap(), DurationTest { timeout: None }, ); } #[test] fn deserialize_none_by_null() { let json = json!({ "timeout": null }); assert_eq!( serde_json::from_value::(json).unwrap(), DurationTest { timeout: None }, ); } #[test] fn serialize_some() { let request = DurationTest { timeout: Some(Duration::new(2, 0)) }; assert_eq!(serde_json::to_value(&request).unwrap(), json!({ "timeout": 2000 })); } #[test] fn serialize_none() { let request = DurationTest { timeout: None }; assert_eq!(serde_json::to_value(&request).unwrap(), json!({})); } } ruma-common-0.10.5/src/serde/duration/secs.rs000064400000000000000000000035111046102023000171620ustar 00000000000000//! De-/serialization functions for `Option` objects represented as //! milliseconds. //! //! Delegates to `js_int::UInt` to ensure integer size is within bounds. use std::time::Duration; use js_int::UInt; use serde::{ de::{Deserialize, Deserializer}, ser::{Error, Serialize, Serializer}, }; /// Serializes a Duration to an integer representing seconds. /// /// Will fail if integer is greater than the maximum integer that can be /// unambiguously represented by an f64. pub fn serialize(duration: &Duration, serializer: S) -> Result where S: Serializer, { match UInt::try_from(duration.as_secs()) { Ok(uint) => uint.serialize(serializer), Err(err) => Err(S::Error::custom(err)), } } /// Deserializes an integer representing seconds into a Duration. /// /// Will fail if integer is greater than the maximum integer that can be /// unambiguously represented by an f64. pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { UInt::deserialize(deserializer).map(|secs| Duration::from_secs(secs.into())) } #[cfg(test)] mod tests { use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] struct DurationTest { #[serde(with = "super")] timeout: Duration, } #[test] fn deserialize() { let json = json!({ "timeout": 3 }); assert_eq!( serde_json::from_value::(json).unwrap(), DurationTest { timeout: Duration::from_secs(3) }, ); } #[test] fn serialize() { let test = DurationTest { timeout: Duration::from_millis(7000) }; assert_eq!(serde_json::to_value(test).unwrap(), json!({ "timeout": 7 })); } } ruma-common-0.10.5/src/serde/duration.rs000064400000000000000000000001411046102023000162210ustar 00000000000000//! De-/serialization functions for `std::time::Duration` objects pub mod opt_ms; pub mod secs; ruma-common-0.10.5/src/serde/empty.rs000064400000000000000000000043141046102023000155400ustar 00000000000000use std::fmt; use serde::{ de::{self, Deserialize}, Serialize, }; #[derive(Clone, Debug, Serialize)] pub struct Empty {} impl<'de> Deserialize<'de> for Empty { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct EmptyMapVisitor; impl<'de> de::Visitor<'de> for EmptyMapVisitor { type Value = Empty; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "an object/map") } fn visit_map(self, _map: A) -> Result where A: de::MapAccess<'de>, { Ok(Empty {}) } } deserializer.deserialize_map(EmptyMapVisitor) } } /// Serde serialization and deserialization functions that map a `Vec` to a /// `BTreeMap`. /// /// The Matrix spec sometimes specifies lists as hash maps so the list entries /// can be expanded with attributes without breaking compatibility. As that /// would be a breaking change for ruma's event types anyway, we convert them to /// `Vec`s for simplicity, using this module. /// /// To be used as `#[serde(with = "vec_as_map_of_empty")]`. pub mod vec_as_map_of_empty { use std::collections::BTreeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::Empty; /// Serialize the given `Vec` as a map of `T => Empty`. #[allow(clippy::ptr_arg)] pub fn serialize(vec: &Vec, serializer: S) -> Result where S: Serializer, T: Serialize + Eq + Ord, { // FIXME: Don't construct a temporary `BTreeMap`. vec.iter().map(|v| (v, Empty {})).collect::>().serialize(serializer) } /// Deserialize an object and return the keys as a `Vec`. pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de> + Eq + Ord, { // FIXME: Don't construct a temporary `BTreeMap`. BTreeMap::::deserialize(deserializer) .map(|hashmap| hashmap.into_iter().map(|(k, _)| k).collect()) } } ruma-common-0.10.5/src/serde/json_string.rs000064400000000000000000000014511046102023000167400ustar 00000000000000//! De-/serialization functions to and from json strings, allows the type to be used as a query //! string. use serde::{ de::{DeserializeOwned, Deserializer, Error as _}, ser::{Error as _, Serialize, Serializer}, }; /// Serialize the given value as a JSON string. pub fn serialize(value: T, serializer: S) -> Result where T: Serialize, S: Serializer, { let json = serde_json::to_string(&value).map_err(S::Error::custom)?; serializer.serialize_str(&json) } /// Read a string from the input and deserialize it as a `T`. pub fn deserialize<'de, T, D>(deserializer: D) -> Result where T: DeserializeOwned, D: Deserializer<'de>, { let s = super::deserialize_cow_str(deserializer)?; serde_json::from_str(&s).map_err(D::Error::custom) } ruma-common-0.10.5/src/serde/raw.rs000064400000000000000000000204561046102023000152000ustar 00000000000000use std::{ clone::Clone, fmt::{self, Debug}, marker::PhantomData, mem, }; use serde::{ de::{self, Deserialize, DeserializeSeed, Deserializer, IgnoredAny, MapAccess, Visitor}, ser::{Serialize, Serializer}, }; use serde_json::value::{to_raw_value as to_raw_json_value, RawValue as RawJsonValue}; /// A wrapper around `Box`, to be used in place of any type in the Matrix endpoint /// definition to allow request and response types to contain that said type represented by /// the generic argument `Ev`. /// /// Ruma offers the `Raw` wrapper to enable passing around JSON text that is only partially /// validated. This is useful when a client receives events that do not follow the spec perfectly /// or a server needs to generate reference hashes with the original canonical JSON string. /// All event structs and enums implement `Serialize` / `Deserialize`, `Raw` should be used /// to pass around events in a lossless way. /// /// ```no_run /// # use serde::Deserialize; /// # use ruma_common::serde::Raw; /// # #[derive(Deserialize)] /// # struct AnyTimelineEvent; /// /// let json = r#"{ "type": "imagine a full event", "content": {...} }"#; /// /// let deser = serde_json::from_str::>(json) /// .unwrap() // the first Result from serde_json::from_str, will not fail /// .deserialize() // deserialize to the inner type /// .unwrap(); // finally get to the AnyTimelineEvent /// ``` #[repr(transparent)] pub struct Raw { json: Box, _ev: PhantomData, } impl Raw { /// Create a `Raw` by serializing the given `T`. /// /// Shorthand for `serde_json::value::to_raw_value(val).map(Raw::from_json)`, but specialized to /// `T`. /// /// # Errors /// /// Fails if `T`s [`Serialize`] implementation fails. pub fn new(val: &T) -> serde_json::Result where T: Serialize, { to_raw_json_value(val).map(Self::from_json) } /// Create a `Raw` from a boxed `RawValue`. pub fn from_json(json: Box) -> Self { Self { json, _ev: PhantomData } } /// Convert an owned `String` of JSON data to `Raw`. /// /// This function is equivalent to `serde_json::from_str::>` except that an allocation /// and copy is avoided if both of the following are true: /// /// * the input has no leading or trailing whitespace, and /// * the input has capacity equal to its length. pub fn from_json_string(json: String) -> serde_json::Result { RawJsonValue::from_string(json).map(Self::from_json) } /// Access the underlying json value. pub fn json(&self) -> &RawJsonValue { &self.json } /// Convert `self` into the underlying json value. pub fn into_json(self) -> Box { self.json } /// Try to access a given field inside this `Raw`, assuming it contains an object. /// /// Returns `Err(_)` when the contained value is not an object, or the field exists but is fails /// to deserialize to the expected type. /// /// Returns `Ok(None)` when the field doesn't exist or is `null`. /// /// # Example /// /// ```no_run /// # type CustomMatrixEvent = (); /// # fn foo() -> serde_json::Result<()> { /// # let raw_event: ruma_common::serde::Raw<()> = todo!(); /// if raw_event.get_field::("type")?.as_deref() == Some("org.custom.matrix.event") { /// let event = raw_event.deserialize_as::()?; /// // ... /// } /// # Ok(()) /// # } /// ``` pub fn get_field<'a, U>(&'a self, field_name: &str) -> serde_json::Result> where U: Deserialize<'a>, { struct FieldVisitor<'b>(&'b str); impl<'b, 'de> Visitor<'de> for FieldVisitor<'b> { type Value = bool; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "`{}`", self.0) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(value == self.0) } } struct Field<'b>(&'b str); impl<'b, 'de> DeserializeSeed<'de> for Field<'b> { type Value = bool; fn deserialize(self, deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_identifier(FieldVisitor(self.0)) } } struct SingleFieldVisitor<'b, T> { field_name: &'b str, _phantom: PhantomData, } impl<'b, T> SingleFieldVisitor<'b, T> { fn new(field_name: &'b str) -> Self { Self { field_name, _phantom: PhantomData } } } impl<'b, 'de, T> Visitor<'de> for SingleFieldVisitor<'b, T> where T: Deserialize<'de>, { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a string") } fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, { let mut res = None; while let Some(is_right_field) = map.next_key_seed(Field(self.field_name))? { if is_right_field { res = Some(map.next_value()?); } else { map.next_value::()?; } } Ok(res) } } let mut deserializer = serde_json::Deserializer::from_str(self.json().get()); deserializer.deserialize_map(SingleFieldVisitor::new(field_name)) } /// Try to deserialize the JSON as the expected type. pub fn deserialize<'a>(&'a self) -> serde_json::Result where T: Deserialize<'a>, { serde_json::from_str(self.json.get()) } /// Try to deserialize the JSON as a custom type. pub fn deserialize_as<'a, U>(&'a self) -> serde_json::Result where U: Deserialize<'a>, { serde_json::from_str(self.json.get()) } /// Turns `Raw` into `Raw` without changing the underlying JSON. /// /// This is useful for turning raw specific event types into raw event enum types. pub fn cast(self) -> Raw { Raw::from_json(self.into_json()) } /// Turns `&Raw` into `&Raw` without changing the underlying JSON. /// /// This is useful for turning raw specific event types into raw event enum types. pub fn cast_ref(&self) -> &Raw { unsafe { mem::transmute(self) } } } impl Clone for Raw { fn clone(&self) -> Self { Self::from_json(self.json.clone()) } } impl Debug for Raw { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use std::any::type_name; f.debug_struct(&format!("Raw::<{}>", type_name::())).field("json", &self.json).finish() } } impl<'de, T> Deserialize<'de> for Raw { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Box::::deserialize(deserializer).map(Self::from_json) } } impl Serialize for Raw { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.json.serialize(serializer) } } #[cfg(test)] mod tests { use serde::Deserialize; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; use super::Raw; #[test] fn get_field() -> serde_json::Result<()> { #[derive(Debug, PartialEq, Deserialize)] struct A<'a> { #[serde(borrow)] b: Vec<&'a str>, } const OBJ: &str = r#"{ "a": { "b": [ "c"] }, "z": 5 }"#; let raw: Raw<()> = from_json_str(OBJ)?; assert_eq!(raw.get_field::("z")?, Some(5)); assert_eq!(raw.get_field::<&RawJsonValue>("a")?.unwrap().get(), r#"{ "b": [ "c"] }"#); assert_eq!(raw.get_field::>("a")?, Some(A { b: vec!["c"] })); assert_eq!(raw.get_field::("b")?, None); raw.get_field::("a").unwrap_err(); Ok(()) } } ruma-common-0.10.5/src/serde/single_element_seq.rs000064400000000000000000000011621046102023000202420ustar 00000000000000//! De-/serialization functions to and from single element sequences. use serde::{ de::{Deserialize, Deserializer}, ser::{Serialize, Serializer}, }; /// Serialize the given value as a list of just that value. pub fn serialize(value: &T, serializer: S) -> Result where T: Serialize, S: Serializer, { [value].serialize(serializer) } /// Deserialize a list of one item and return that item. pub fn deserialize<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de>, D: Deserializer<'de>, { <[_; 1]>::deserialize(deserializer).map(|[first]| first) } ruma-common-0.10.5/src/serde/strings.rs000064400000000000000000000142121046102023000160710ustar 00000000000000use std::{collections::BTreeMap, fmt, marker::PhantomData}; use js_int::{Int, UInt}; use serde::{ de::{self, Deserializer, IntoDeserializer as _, MapAccess, Visitor}, ser::Serializer, Deserialize, Serialize, }; /// Serde deserialization decorator to map empty Strings to None, /// and forward non-empty Strings to the Deserialize implementation for T. /// Useful for the typical /// "A room with an X event with an absent, null, or empty Y field /// should be treated the same as a room with no such event." /// formulation in the spec. /// /// To be used like this: /// `#[serde(default, deserialize_with = "empty_string_as_none")]` /// Relevant serde issue: pub fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de>, { let opt = Option::::deserialize(de)?; match opt.as_deref() { None | Some("") => Ok(None), // If T = String, like in m.room.name, the second deserialize is actually superfluous. // TODO: optimize that somehow? Some(s) => T::deserialize(s.into_deserializer()).map(Some), } } /// Serde serializiation decorator to map `None` to an empty `String`, /// and forward `Some`s to the `Serialize` implementation for `T`. /// /// To be used like this: /// `#[serde(serialize_with = "empty_string_as_none")]` pub fn none_as_empty_string( value: &Option, serializer: S, ) -> Result where S: Serializer, { match value { Some(x) => x.serialize(serializer), None => serializer.serialize_str(""), } } /// Take either an integer number or a string and deserialize to an integer number. /// /// To be used like this: /// `#[serde(deserialize_with = "deserialize_v1_powerlevel")]` pub fn deserialize_v1_powerlevel<'de, D>(de: D) -> Result where D: Deserializer<'de>, { struct IntOrStringVisitor; impl<'de> Visitor<'de> for IntOrStringVisitor { type Value = Int; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("an integer or a string") } fn visit_i8(self, v: i8) -> Result { Ok(v.into()) } fn visit_i16(self, v: i16) -> Result { Ok(v.into()) } fn visit_i32(self, v: i32) -> Result { Ok(v.into()) } fn visit_i64(self, v: i64) -> Result { v.try_into().map_err(E::custom) } fn visit_i128(self, v: i128) -> Result { v.try_into().map_err(E::custom) } fn visit_u8(self, v: u8) -> Result { Ok(v.into()) } fn visit_u16(self, v: u16) -> Result { Ok(v.into()) } fn visit_u32(self, v: u32) -> Result { Ok(v.into()) } fn visit_u64(self, v: u64) -> Result { v.try_into().map_err(E::custom) } fn visit_u128(self, v: u128) -> Result { v.try_into().map_err(E::custom) } fn visit_str(self, v: &str) -> Result { let trimmed = v.trim(); match trimmed.strip_prefix('+') { Some(without) => without.parse::().map(|u| u.into()).map_err(E::custom), None => trimmed.parse().map_err(E::custom), } } } de.deserialize_any(IntOrStringVisitor) } /// Take a BTreeMap with values of either an integer number or a string and deserialize /// those to integer numbers. /// /// To be used like this: /// `#[serde(deserialize_with = "btreemap_deserialize_v1_powerlevel_values")]` pub fn btreemap_deserialize_v1_powerlevel_values<'de, D, T>( de: D, ) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de> + Ord, { #[repr(transparent)] struct IntWrap(Int); impl<'de> Deserialize<'de> for IntWrap { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserialize_v1_powerlevel(deserializer).map(IntWrap) } } struct IntMapVisitor { _phantom: PhantomData, } impl IntMapVisitor { fn new() -> Self { Self { _phantom: PhantomData } } } impl<'de, T> Visitor<'de> for IntMapVisitor where T: Deserialize<'de> + Ord, { type Value = BTreeMap; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a map with integers or stings as values") } fn visit_map>(self, mut map: A) -> Result { let mut res = BTreeMap::new(); while let Some((k, IntWrap(v))) = map.next_entry()? { res.insert(k, v); } Ok(res) } } de.deserialize_map(IntMapVisitor::new()) } #[cfg(test)] mod tests { use js_int::{int, Int}; use serde::Deserialize; use super::deserialize_v1_powerlevel; #[derive(Debug, Deserialize)] struct Test { #[serde(deserialize_with = "deserialize_v1_powerlevel")] num: Int, } #[test] fn int_or_string() { let test = serde_json::from_value::(serde_json::json!({ "num": "0" })).unwrap(); assert_eq!(test.num, int!(0)); } #[test] fn weird_plus_string() { let test = serde_json::from_value::(serde_json::json!({ "num": " +0000000001000 " })) .unwrap(); assert_eq!(test.num, int!(1000)); } #[test] fn weird_minus_string() { let test = serde_json::from_value::( serde_json::json!({ "num": " \n\n-0000000000000001000 " }), ) .unwrap(); assert_eq!(test.num, int!(-1000)); } } ruma-common-0.10.5/src/serde/test.rs000064400000000000000000000006701046102023000153620ustar 00000000000000//! Helpers for tests use std::fmt::Debug; use serde::{de::DeserializeOwned, Serialize}; /// Assert that serialization of `de` results in `se` and deserialization of `se` results in `de`. pub fn serde_json_eq(de: T, se: serde_json::Value) where T: Clone + Debug + PartialEq + Serialize + DeserializeOwned, { assert_eq!(se, serde_json::to_value(de.clone()).unwrap()); assert_eq!(de, serde_json::from_value(se).unwrap()); } ruma-common-0.10.5/src/serde/urlencoded/de/val_or_vec.rs000064400000000000000000000150621046102023000212370ustar 00000000000000use std::{iter, ptr, vec}; use serde::de::{ self, value::{Error, SeqDeserializer}, Deserializer, IntoDeserializer, }; #[derive(Debug)] pub enum ValOrVec { Val(T), Vec(Vec), } impl ValOrVec { pub fn push(&mut self, new_val: T) { match self { // To transform a Self::Val into a Self::Vec, we take the existing // value out via ptr::read and add it to a vector, together with the // new value. Since setting self to `ValOrVec::Vec` normally would // cause T's Drop implementation to run if it has one (which would // free resources that will now be owned by the first vec element), // we instead use ptr::write to set self to Self::Vec. ValOrVec::Val(val) => { let mut vec = Vec::with_capacity(2); // Safety: since the vec is pre-allocated, push can't panic, so // there is no opportunity for outside code to observe an // invalid state of self. unsafe { let existing_val = ptr::read(val); vec.push(existing_val); vec.push(new_val); ptr::write(self, ValOrVec::Vec(vec)); } } ValOrVec::Vec(vec) => vec.push(new_val), } } fn deserialize_val(self, f: F) -> Result where F: FnOnce(T) -> Result, E: de::Error, { match self { ValOrVec::Val(val) => f(val), ValOrVec::Vec(_) => Err(de::Error::custom("unsupported value")), } } } impl IntoIterator for ValOrVec { type Item = T; type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { IntoIter::new(self) } } pub enum IntoIter { Val(iter::Once), Vec(vec::IntoIter), } impl IntoIter { fn new(vv: ValOrVec) -> Self { match vv { ValOrVec::Val(val) => IntoIter::Val(iter::once(val)), ValOrVec::Vec(vec) => IntoIter::Vec(vec.into_iter()), } } } impl Iterator for IntoIter { type Item = T; fn next(&mut self) -> Option { match self { IntoIter::Val(iter) => iter.next(), IntoIter::Vec(iter) => iter.next(), } } } impl<'de, T> IntoDeserializer<'de> for ValOrVec where T: IntoDeserializer<'de> + Deserializer<'de, Error = Error>, { type Deserializer = Self; fn into_deserializer(self) -> Self::Deserializer { self } } macro_rules! forward_to_part { ($($method:ident,)*) => { $( fn $method(self, visitor: V) -> Result where V: de::Visitor<'de> { self.deserialize_val(move |val| val.$method(visitor)) } )* } } impl<'de, T> Deserializer<'de> for ValOrVec where T: IntoDeserializer<'de> + Deserializer<'de, Error = Error>, { type Error = Error; fn deserialize_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { match self { ValOrVec::Val(val) => val.deserialize_any(visitor), ValOrVec::Vec(_) => self.deserialize_seq(visitor), } } fn deserialize_seq(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_seq(SeqDeserializer::new(self.into_iter())) } fn deserialize_enum( self, name: &'static str, variants: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_val(move |val| val.deserialize_enum(name, variants, visitor)) } fn deserialize_tuple(self, len: usize, visitor: V) -> Result where V: de::Visitor<'de>, { self.deserialize_val(move |val| val.deserialize_tuple(len, visitor)) } fn deserialize_struct( self, name: &'static str, fields: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_val(move |val| val.deserialize_struct(name, fields, visitor)) } fn deserialize_unit_struct( self, name: &'static str, visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_val(move |val| val.deserialize_unit_struct(name, visitor)) } fn deserialize_tuple_struct( self, name: &'static str, len: usize, visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_val(move |val| val.deserialize_tuple_struct(name, len, visitor)) } fn deserialize_newtype_struct( self, name: &'static str, visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_val(move |val| val.deserialize_newtype_struct(name, visitor)) } fn deserialize_ignored_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_unit() } forward_to_part! { deserialize_bool, deserialize_char, deserialize_str, deserialize_string, deserialize_bytes, deserialize_byte_buf, deserialize_unit, deserialize_u8, deserialize_u16, deserialize_u32, deserialize_u64, deserialize_i8, deserialize_i16, deserialize_i32, deserialize_i64, deserialize_f32, deserialize_f64, deserialize_option, deserialize_identifier, deserialize_map, } } #[cfg(test)] mod tests { use std::borrow::Cow; use assert_matches::assert_matches; use super::ValOrVec; #[test] fn cow_borrowed() { let mut x = ValOrVec::Val(Cow::Borrowed("a")); x.push(Cow::Borrowed("b")); x.push(Cow::Borrowed("c")); let v = assert_matches!(x, ValOrVec::Vec(v) => v); assert_eq!(v, vec!["a", "b", "c"]); } #[test] fn cow_owned() { let mut x = ValOrVec::Val(Cow::from("a".to_owned())); x.push(Cow::from("b".to_owned())); x.push(Cow::from("c".to_owned())); let v = assert_matches!( x, ValOrVec::Vec(v) => v ); assert_eq!(v, vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]); } } ruma-common-0.10.5/src/serde/urlencoded/de.rs000064400000000000000000000203321046102023000171140ustar 00000000000000//! Deserialization support for the `application/x-www-form-urlencoded` format. use std::{ borrow::Cow, collections::btree_map::{self, BTreeMap}, io::Read, }; use form_urlencoded::{parse, Parse as UrlEncodedParse}; use serde::{ de::{self, value::MapDeserializer, Error as de_Error, IntoDeserializer}, forward_to_deserialize_any, }; #[doc(inline)] pub use serde::de::value::Error; mod val_or_vec; use val_or_vec::ValOrVec; /// Deserializes a `application/x-www-form-urlencoded` value from a `&[u8]`. /// /// ``` /// let meal = vec![ /// ("bread".to_owned(), "baguette".to_owned()), /// ("cheese".to_owned(), "comté".to_owned()), /// ("fat".to_owned(), "butter".to_owned()), /// ("meat".to_owned(), "ham".to_owned()), /// ]; /// /// assert_eq!( /// ruma_common::serde::urlencoded::from_bytes::>( /// b"bread=baguette&cheese=comt%C3%A9&meat=ham&fat=butter" /// ), /// Ok(meal) /// ); /// ``` pub fn from_bytes<'de, T>(input: &'de [u8]) -> Result where T: de::Deserialize<'de>, { T::deserialize(Deserializer::new(parse(input))) } /// Deserializes a `application/x-www-form-urlencoded` value from a `&str`. /// /// ``` /// let meal = vec![ /// ("bread".to_owned(), "baguette".to_owned()), /// ("cheese".to_owned(), "comté".to_owned()), /// ("fat".to_owned(), "butter".to_owned()), /// ("meat".to_owned(), "ham".to_owned()), /// ]; /// /// assert_eq!( /// ruma_common::serde::urlencoded::from_str::>( /// "bread=baguette&cheese=comt%C3%A9&meat=ham&fat=butter" /// ), /// Ok(meal) /// ); /// ``` pub fn from_str<'de, T>(input: &'de str) -> Result where T: de::Deserialize<'de>, { from_bytes(input.as_bytes()) } /// Convenience function that reads all bytes from `reader` and deserializes /// them with `from_bytes`. pub fn from_reader(mut reader: R) -> Result where T: de::DeserializeOwned, R: Read, { let mut buf = vec![]; reader .read_to_end(&mut buf) .map_err(|e| de::Error::custom(format_args!("could not read input: {e}")))?; from_bytes(&buf) } /// A deserializer for the `application/x-www-form-urlencoded` format. /// /// * Supported top-level outputs are structs, maps and sequences of pairs, with or without a given /// length. /// /// * Main `deserialize` methods defers to `deserialize_map`. /// /// * Everything else but `deserialize_seq` and `deserialize_seq_fixed_size` defers to /// `deserialize`. pub struct Deserializer<'de> { inner: MapDeserializer<'de, EntryIterator<'de>, Error>, } impl<'de> Deserializer<'de> { /// Returns a new `Deserializer`. pub fn new(parse: UrlEncodedParse<'de>) -> Self { Deserializer { inner: MapDeserializer::new(group_entries(parse).into_iter()) } } } impl<'de> de::Deserializer<'de> for Deserializer<'de> { type Error = Error; fn deserialize_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { self.deserialize_map(visitor) } fn deserialize_map(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_map(self.inner) } fn deserialize_seq(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_seq(self.inner) } fn deserialize_unit(self, visitor: V) -> Result where V: de::Visitor<'de>, { self.inner.end()?; visitor.visit_unit() } forward_to_deserialize_any! { bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string option bytes byte_buf unit_struct newtype_struct tuple_struct struct identifier tuple enum ignored_any } } fn group_entries(parse: UrlEncodedParse<'_>) -> BTreeMap, ValOrVec>> { use btree_map::Entry::*; let mut res = BTreeMap::new(); for (key, value) in parse { match res.entry(Part(key)) { Vacant(v) => { v.insert(ValOrVec::Val(Part(value))); } Occupied(mut o) => { o.get_mut().push(Part(value)); } } } res } /* input: a=b&c=d&a=c vvvvv next(): a => Wrapper([b, c]) next(): c => Wrapper([d]) struct Foo { a: Vec, c: Vec, } struct Bar { a: Vec, c: String, } struct Baz { a: String, } */ type EntryIterator<'de> = btree_map::IntoIter, ValOrVec>>; #[derive(PartialEq, PartialOrd, Eq, Ord)] struct Part<'de>(Cow<'de, str>); impl<'de> IntoDeserializer<'de> for Part<'de> { type Deserializer = Self; fn into_deserializer(self) -> Self::Deserializer { self } } macro_rules! forward_parsed_value { ($($ty:ident => $method:ident,)*) => { $( fn $method(self, visitor: V) -> Result where V: de::Visitor<'de> { match self.0.parse::<$ty>() { Ok(val) => val.into_deserializer().$method(visitor), Err(e) => Err(de::Error::custom(e)) } } )* } } impl<'de> de::Deserializer<'de> for Part<'de> { type Error = Error; fn deserialize_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { match self.0 { Cow::Borrowed(value) => visitor.visit_borrowed_str(value), Cow::Owned(value) => visitor.visit_string(value), } } fn deserialize_option(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_some(self) } fn deserialize_enum( self, _name: &'static str, _variants: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { visitor.visit_enum(ValueEnumAccess(self.0)) } fn deserialize_newtype_struct( self, _name: &'static str, visitor: V, ) -> Result where V: de::Visitor<'de>, { visitor.visit_newtype_struct(self) } forward_to_deserialize_any! { char str string unit bytes byte_buf unit_struct tuple_struct struct identifier tuple ignored_any seq map } forward_parsed_value! { bool => deserialize_bool, u8 => deserialize_u8, u16 => deserialize_u16, u32 => deserialize_u32, u64 => deserialize_u64, i8 => deserialize_i8, i16 => deserialize_i16, i32 => deserialize_i32, i64 => deserialize_i64, f32 => deserialize_f32, f64 => deserialize_f64, } } struct ValueEnumAccess<'de>(Cow<'de, str>); impl<'de> de::EnumAccess<'de> for ValueEnumAccess<'de> { type Error = Error; type Variant = UnitOnlyVariantAccess; fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> where V: de::DeserializeSeed<'de>, { let variant = seed.deserialize(self.0.into_deserializer())?; Ok((variant, UnitOnlyVariantAccess)) } } struct UnitOnlyVariantAccess; impl<'de> de::VariantAccess<'de> for UnitOnlyVariantAccess { type Error = Error; fn unit_variant(self) -> Result<(), Self::Error> { Ok(()) } fn newtype_variant_seed(self, _seed: T) -> Result where T: de::DeserializeSeed<'de>, { Err(Error::custom("expected unit variant")) } fn tuple_variant(self, _len: usize, _visitor: V) -> Result where V: de::Visitor<'de>, { Err(Error::custom("expected unit variant")) } fn struct_variant( self, _fields: &'static [&'static str], _visitor: V, ) -> Result where V: de::Visitor<'de>, { Err(Error::custom("expected unit variant")) } } ruma-common-0.10.5/src/serde/urlencoded/ser/key.rs000064400000000000000000000033501046102023000201060ustar 00000000000000use std::{borrow::Cow, ops::Deref}; use serde::ser; use super::{part::Sink, Error}; pub enum Key<'key> { Static(&'static str), Dynamic(Cow<'key, str>), } impl<'key> Deref for Key<'key> { type Target = str; fn deref(&self) -> &str { match *self { Key::Static(key) => key, Key::Dynamic(ref key) => key, } } } impl<'key> From> for Cow<'static, str> { fn from(key: Key<'key>) -> Self { match key { Key::Static(key) => key.into(), Key::Dynamic(key) => key.into_owned().into(), } } } pub struct KeySink { end: End, } impl KeySink where End: for<'key> FnOnce(Key<'key>) -> Result, { pub fn new(end: End) -> Self { KeySink { end } } } impl Sink for KeySink where End: for<'key> FnOnce(Key<'key>) -> Result, { type Ok = Ok; type SerializeSeq = ser::Impossible; fn serialize_static_str(self, value: &'static str) -> Result { (self.end)(Key::Static(value)) } fn serialize_str(self, value: &str) -> Result { (self.end)(Key::Dynamic(value.into())) } fn serialize_string(self, value: String) -> Result { (self.end)(Key::Dynamic(value.into())) } fn serialize_none(self) -> Result { Err(self.unsupported()) } fn serialize_some(self, _value: &T) -> Result { Err(self.unsupported()) } fn serialize_seq(self) -> Result { Err(self.unsupported()) } fn unsupported(self) -> Error { Error::Custom("unsupported key".into()) } } ruma-common-0.10.5/src/serde/urlencoded/ser/pair.rs000064400000000000000000000155041046102023000202550ustar 00000000000000use std::{borrow::Cow, mem}; use form_urlencoded::{Serializer as UrlEncodedSerializer, Target as UrlEncodedTarget}; use serde::ser; use super::{key::KeySink, part::PartSerializer, value::ValueSink, Error}; pub struct PairSerializer<'input, 'target, Target: UrlEncodedTarget> { urlencoder: &'target mut UrlEncodedSerializer<'input, Target>, state: PairState, } impl<'input, 'target, Target> PairSerializer<'input, 'target, Target> where Target: 'target + UrlEncodedTarget, { pub fn new(urlencoder: &'target mut UrlEncodedSerializer<'input, Target>) -> Self { PairSerializer { urlencoder, state: PairState::WaitingForKey } } } impl<'input, 'target, Target> ser::Serializer for PairSerializer<'input, 'target, Target> where Target: 'target + UrlEncodedTarget, { type Ok = (); type Error = Error; type SerializeSeq = ser::Impossible<(), Error>; type SerializeTuple = Self; type SerializeTupleStruct = ser::Impossible<(), Error>; type SerializeTupleVariant = ser::Impossible<(), Error>; type SerializeMap = ser::Impossible<(), Error>; type SerializeStruct = ser::Impossible<(), Error>; type SerializeStructVariant = ser::Impossible<(), Error>; fn serialize_bool(self, _v: bool) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_i8(self, _v: i8) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_i16(self, _v: i16) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_i32(self, _v: i32) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_i64(self, _v: i64) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_u8(self, _v: u8) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_u16(self, _v: u16) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_u32(self, _v: u32) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_u64(self, _v: u64) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_f32(self, _v: f32) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_f64(self, _v: f64) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_char(self, _v: char) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_str(self, _value: &str) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_bytes(self, _value: &[u8]) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_unit(self) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, ) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_newtype_struct( self, _name: &'static str, value: &T, ) -> Result<(), Error> { value.serialize(self) } fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _value: &T, ) -> Result<(), Error> { Err(Error::unsupported_pair()) } fn serialize_none(self) -> Result<(), Error> { Ok(()) } fn serialize_some(self, value: &T) -> Result<(), Error> { value.serialize(self) } fn serialize_seq(self, _len: Option) -> Result { Err(Error::unsupported_pair()) } fn serialize_tuple(self, len: usize) -> Result { if len == 2 { Ok(self) } else { Err(Error::unsupported_pair()) } } fn serialize_tuple_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(Error::unsupported_pair()) } fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { Err(Error::unsupported_pair()) } fn serialize_map(self, _len: Option) -> Result { Err(Error::unsupported_pair()) } fn serialize_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(Error::unsupported_pair()) } fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { Err(Error::unsupported_pair()) } } impl<'input, 'target, Target> ser::SerializeTuple for PairSerializer<'input, 'target, Target> where Target: 'target + UrlEncodedTarget, { type Ok = (); type Error = Error; fn serialize_element(&mut self, value: &T) -> Result<(), Error> { match mem::replace(&mut self.state, PairState::Done) { PairState::WaitingForKey => { let key_sink = KeySink::new(|key| Ok(key.into())); let key_serializer = PartSerializer::new(key_sink); self.state = PairState::WaitingForValue { key: value.serialize(key_serializer)? }; Ok(()) } PairState::WaitingForValue { key } => { let result = { let value_sink = ValueSink::new(self.urlencoder, &key); let value_serializer = PartSerializer::new(value_sink); value.serialize(value_serializer) }; if result.is_ok() { self.state = PairState::Done; } else { self.state = PairState::WaitingForValue { key }; } result } PairState::Done => Err(Error::done()), } } fn end(self) -> Result<(), Error> { if let PairState::Done = self.state { Ok(()) } else { Err(Error::not_done()) } } } enum PairState { WaitingForKey, WaitingForValue { key: Cow<'static, str> }, Done, } impl Error { fn done() -> Self { Error::Custom("this pair has already been serialized".into()) } fn not_done() -> Self { Error::Custom("this pair has not yet been serialized".into()) } fn unsupported_pair() -> Self { Error::Custom("unsupported pair".into()) } } ruma-common-0.10.5/src/serde/urlencoded/ser/part.rs000064400000000000000000000127661046102023000202770ustar 00000000000000use std::str; use serde::ser; use super::Error; pub struct PartSerializer { sink: S, } impl PartSerializer { pub fn new(sink: S) -> Self { PartSerializer { sink } } } pub trait Sink: Sized { type Ok; type SerializeSeq: ser::SerializeSeq; fn serialize_static_str(self, value: &'static str) -> Result; fn serialize_str(self, value: &str) -> Result; fn serialize_string(self, value: String) -> Result; fn serialize_none(self) -> Result; fn serialize_some(self, value: &T) -> Result; fn serialize_seq(self) -> Result; fn unsupported(self) -> Error; } impl ser::Serializer for PartSerializer { type Ok = S::Ok; type Error = Error; type SerializeSeq = S::SerializeSeq; type SerializeTuple = ser::Impossible; type SerializeTupleStruct = ser::Impossible; type SerializeTupleVariant = ser::Impossible; type SerializeMap = ser::Impossible; type SerializeStruct = ser::Impossible; type SerializeStructVariant = ser::Impossible; fn serialize_bool(self, v: bool) -> Result { self.sink.serialize_static_str(if v { "true" } else { "false" }) } fn serialize_i8(self, v: i8) -> Result { self.serialize_integer(v) } fn serialize_i16(self, v: i16) -> Result { self.serialize_integer(v) } fn serialize_i32(self, v: i32) -> Result { self.serialize_integer(v) } fn serialize_i64(self, v: i64) -> Result { self.serialize_integer(v) } fn serialize_u8(self, v: u8) -> Result { self.serialize_integer(v) } fn serialize_u16(self, v: u16) -> Result { self.serialize_integer(v) } fn serialize_u32(self, v: u32) -> Result { self.serialize_integer(v) } fn serialize_u64(self, v: u64) -> Result { self.serialize_integer(v) } fn serialize_f32(self, _v: f32) -> Result { Err(self.sink.unsupported()) } fn serialize_f64(self, _v: f64) -> Result { Err(self.sink.unsupported()) } fn serialize_char(self, v: char) -> Result { self.sink.serialize_string(v.to_string()) } fn serialize_str(self, value: &str) -> Result { self.sink.serialize_str(value) } fn serialize_bytes(self, value: &[u8]) -> Result { match str::from_utf8(value) { Ok(value) => self.sink.serialize_str(value), Err(err) => Err(Error::Utf8(err)), } } fn serialize_unit(self) -> Result { Err(self.sink.unsupported()) } fn serialize_unit_struct(self, name: &'static str) -> Result { self.sink.serialize_static_str(name) } fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, ) -> Result { self.sink.serialize_static_str(variant) } fn serialize_newtype_struct( self, _name: &'static str, value: &T, ) -> Result { value.serialize(self) } fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _value: &T, ) -> Result { Err(self.sink.unsupported()) } fn serialize_none(self) -> Result { self.sink.serialize_none() } fn serialize_some(self, value: &T) -> Result { self.sink.serialize_some(value) } fn serialize_seq(self, _len: Option) -> Result { self.sink.serialize_seq() } fn serialize_tuple(self, _len: usize) -> Result { Err(self.sink.unsupported()) } fn serialize_tuple_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(self.sink.unsupported()) } fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { Err(self.sink.unsupported()) } fn serialize_map(self, _len: Option) -> Result { Err(self.sink.unsupported()) } fn serialize_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(self.sink.unsupported()) } fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { Err(self.sink.unsupported()) } } impl PartSerializer { fn serialize_integer(self, value: I) -> Result where I: itoa::Integer, { let mut buf = itoa::Buffer::new(); let part = buf.format(value); ser::Serializer::serialize_str(self, part) } } ruma-common-0.10.5/src/serde/urlencoded/ser/value.rs000064400000000000000000000043761046102023000204430ustar 00000000000000use std::str; use form_urlencoded::{Serializer as UrlEncodedSerializer, Target as UrlEncodedTarget}; use serde::ser; use super::{ part::{PartSerializer, Sink}, Error, }; pub struct ValueSink<'input, 'key, 'target, Target> where Target: UrlEncodedTarget, { urlencoder: &'target mut UrlEncodedSerializer<'input, Target>, key: &'key str, nested: bool, } impl<'input, 'key, 'target, Target> ValueSink<'input, 'key, 'target, Target> where Target: 'target + UrlEncodedTarget, { pub fn new( urlencoder: &'target mut UrlEncodedSerializer<'input, Target>, key: &'key str, ) -> Self { ValueSink { urlencoder, key, nested: false } } } impl<'input, 'key, 'target, Target> Sink for ValueSink<'input, 'key, 'target, Target> where Target: 'target + UrlEncodedTarget, { type Ok = (); type SerializeSeq = Self; fn serialize_str(self, value: &str) -> Result<(), Error> { self.urlencoder.append_pair(self.key, value); Ok(()) } fn serialize_static_str(self, value: &'static str) -> Result<(), Error> { self.serialize_str(value) } fn serialize_string(self, value: String) -> Result<(), Error> { self.serialize_str(&value) } fn serialize_none(self) -> Result { Ok(()) } fn serialize_some(self, value: &T) -> Result { value.serialize(PartSerializer::new(self)) } fn serialize_seq(self) -> Result { if self.nested { Err(self.unsupported()) } else { Ok(self) } } fn unsupported(self) -> Error { Error::Custom("unsupported value".into()) } } impl<'input, 'key, 'target, Target> ser::SerializeSeq for ValueSink<'input, 'key, 'target, Target> where Target: 'target + UrlEncodedTarget, { type Ok = (); type Error = Error; fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where T: ser::Serialize, { value.serialize(PartSerializer::new(ValueSink { urlencoder: self.urlencoder, key: self.key, nested: true, })) } fn end(self) -> Result { Ok(()) } } ruma-common-0.10.5/src/serde/urlencoded/ser.rs000064400000000000000000000344161046102023000173250ustar 00000000000000//! Serialization support for the `application/x-www-form-urlencoded` format. mod key; mod pair; mod part; mod value; use std::{borrow::Cow, error, fmt, str}; use form_urlencoded::{Serializer as UrlEncodedSerializer, Target as UrlEncodedTarget}; use serde::ser; /// Serializes a value into a `application/x-www-form-urlencoded` `String` buffer. /// /// ``` /// let meal = &[("bread", "baguette"), ("cheese", "comté"), ("meat", "ham"), ("fat", "butter")]; /// /// assert_eq!( /// ruma_common::serde::urlencoded::to_string(meal), /// Ok("bread=baguette&cheese=comt%C3%A9&meat=ham&fat=butter".to_owned()) /// ); /// ``` pub fn to_string(input: T) -> Result { let mut urlencoder = UrlEncodedSerializer::new("".to_owned()); input.serialize(Serializer::new(&mut urlencoder))?; Ok(urlencoder.finish()) } /// A serializer for the `application/x-www-form-urlencoded` format. /// /// * Supported top-level inputs are structs, maps and sequences of pairs, with or without a given /// length. /// /// * Supported keys and values are integers, bytes (if convertible to strings), unit structs and /// unit variants. /// /// * Newtype structs defer to their inner values. pub struct Serializer<'input, 'output, Target: UrlEncodedTarget> { urlencoder: &'output mut UrlEncodedSerializer<'input, Target>, } impl<'input, 'output, Target: UrlEncodedTarget> Serializer<'input, 'output, Target> { /// Returns a new `Serializer`. pub fn new(urlencoder: &'output mut UrlEncodedSerializer<'input, Target>) -> Self { Serializer { urlencoder } } } /// Errors returned during serializing to `application/x-www-form-urlencoded`. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Error { /// UTF-8 validation failed. Utf8(str::Utf8Error), /// Something else. Custom(Cow<'static, str>), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Error::Custom(ref msg) => msg.fmt(f), Error::Utf8(ref err) => write!(f, "invalid UTF-8: {err}"), } } } impl error::Error for Error { /// The lower-level cause of this error, in the case of a `Utf8` error. fn cause(&self) -> Option<&dyn error::Error> { match *self { Error::Custom(_) => None, Error::Utf8(ref err) => Some(err), } } /// The lower-level source of this error, in the case of a `Utf8` error. fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { Error::Custom(_) => None, Error::Utf8(ref err) => Some(err), } } } impl ser::Error for Error { fn custom(msg: T) -> Self { Error::Custom(format!("{msg}").into()) } } /// Sequence serializer. pub struct SeqSerializer<'input, 'output, Target: UrlEncodedTarget> { urlencoder: &'output mut UrlEncodedSerializer<'input, Target>, } /// Tuple serializer. /// /// Mostly used for arrays. pub struct TupleSerializer<'input, 'output, Target: UrlEncodedTarget> { urlencoder: &'output mut UrlEncodedSerializer<'input, Target>, } /// Tuple struct serializer. /// /// Never instantiated, tuple structs are not supported. pub struct TupleStructSerializer<'input, 'output, T: UrlEncodedTarget> { inner: ser::Impossible<&'output mut UrlEncodedSerializer<'input, T>, Error>, } /// Tuple variant serializer. /// /// Never instantiated, tuple variants are not supported. pub struct TupleVariantSerializer<'input, 'output, T: UrlEncodedTarget> { inner: ser::Impossible<&'output mut UrlEncodedSerializer<'input, T>, Error>, } /// Map serializer. pub struct MapSerializer<'input, 'output, Target: UrlEncodedTarget> { urlencoder: &'output mut UrlEncodedSerializer<'input, Target>, key: Option>, } /// Struct serializer. pub struct StructSerializer<'input, 'output, Target: UrlEncodedTarget> { urlencoder: &'output mut UrlEncodedSerializer<'input, Target>, } /// Struct variant serializer. /// /// Never instantiated, struct variants are not supported. pub struct StructVariantSerializer<'input, 'output, T: UrlEncodedTarget> { inner: ser::Impossible<&'output mut UrlEncodedSerializer<'input, T>, Error>, } impl<'input, 'output, Target> ser::Serializer for Serializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; type SerializeSeq = SeqSerializer<'input, 'output, Target>; type SerializeTuple = TupleSerializer<'input, 'output, Target>; type SerializeTupleStruct = TupleStructSerializer<'input, 'output, Target>; type SerializeTupleVariant = TupleVariantSerializer<'input, 'output, Target>; type SerializeMap = MapSerializer<'input, 'output, Target>; type SerializeStruct = StructSerializer<'input, 'output, Target>; type SerializeStructVariant = StructVariantSerializer<'input, 'output, Target>; /// Returns an error. fn serialize_bool(self, _v: bool) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_i8(self, _v: i8) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_i16(self, _v: i16) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_i32(self, _v: i32) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_i64(self, _v: i64) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_u8(self, _v: u8) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_u16(self, _v: u16) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_u32(self, _v: u32) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_u64(self, _v: u64) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_f32(self, _v: f32) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_f64(self, _v: f64) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_char(self, _v: char) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_str(self, _value: &str) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_bytes(self, _value: &[u8]) -> Result { Err(Error::top_level()) } /// Returns `Ok`. fn serialize_unit(self) -> Result { Ok(self.urlencoder) } /// Returns `Ok`. fn serialize_unit_struct(self, _name: &'static str) -> Result { Ok(self.urlencoder) } /// Returns an error. fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, ) -> Result { Err(Error::top_level()) } /// Serializes the inner value, ignoring the newtype name. fn serialize_newtype_struct( self, _name: &'static str, value: &T, ) -> Result { value.serialize(self) } /// Returns an error. fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _value: &T, ) -> Result { Err(Error::top_level()) } /// Returns `Ok`. fn serialize_none(self) -> Result { Ok(self.urlencoder) } /// Serializes the given value. fn serialize_some(self, value: &T) -> Result { value.serialize(self) } /// Serialize a sequence, given length (if any) is ignored. fn serialize_seq(self, _len: Option) -> Result { Ok(SeqSerializer { urlencoder: self.urlencoder }) } /// Returns an error. fn serialize_tuple(self, _len: usize) -> Result { Ok(TupleSerializer { urlencoder: self.urlencoder }) } /// Returns an error. fn serialize_tuple_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(Error::top_level()) } /// Returns an error. fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { Err(Error::top_level()) } /// Serializes a map, given length is ignored. fn serialize_map(self, _len: Option) -> Result { Ok(MapSerializer { urlencoder: self.urlencoder, key: None }) } /// Serializes a struct, given length is ignored. fn serialize_struct( self, _name: &'static str, _len: usize, ) -> Result { Ok(StructSerializer { urlencoder: self.urlencoder }) } /// Returns an error. fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { Err(Error::top_level()) } } impl<'input, 'output, Target> ser::SerializeSeq for SeqSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_element(&mut self, value: &T) -> Result<(), Error> { value.serialize(pair::PairSerializer::new(self.urlencoder)) } fn end(self) -> Result { Ok(self.urlencoder) } } impl<'input, 'output, Target> ser::SerializeTuple for TupleSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_element(&mut self, value: &T) -> Result<(), Error> { value.serialize(pair::PairSerializer::new(self.urlencoder)) } fn end(self) -> Result { Ok(self.urlencoder) } } impl<'input, 'output, Target> ser::SerializeTupleStruct for TupleStructSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_field(&mut self, value: &T) -> Result<(), Error> { self.inner.serialize_field(value) } fn end(self) -> Result { self.inner.end() } } impl<'input, 'output, Target> ser::SerializeTupleVariant for TupleVariantSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_field(&mut self, value: &T) -> Result<(), Error> { self.inner.serialize_field(value) } fn end(self) -> Result { self.inner.end() } } impl<'input, 'output, Target> ser::SerializeMap for MapSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_entry( &mut self, key: &K, value: &V, ) -> Result<(), Error> { let key_sink = key::KeySink::new(|key| { let value_sink = value::ValueSink::new(self.urlencoder, &key); value.serialize(part::PartSerializer::new(value_sink))?; self.key = None; Ok(()) }); let entry_serializer = part::PartSerializer::new(key_sink); key.serialize(entry_serializer) } fn serialize_key(&mut self, key: &T) -> Result<(), Error> { let key_sink = key::KeySink::new(|key| Ok(key.into())); let key_serializer = part::PartSerializer::new(key_sink); self.key = Some(key.serialize(key_serializer)?); Ok(()) } fn serialize_value(&mut self, value: &T) -> Result<(), Error> { { let key = self.key.as_ref().ok_or_else(Error::no_key)?; let value_sink = value::ValueSink::new(self.urlencoder, key); value.serialize(part::PartSerializer::new(value_sink))?; } self.key = None; Ok(()) } fn end(self) -> Result { Ok(self.urlencoder) } } impl<'input, 'output, Target> ser::SerializeStruct for StructSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_field( &mut self, key: &'static str, value: &T, ) -> Result<(), Error> { let value_sink = value::ValueSink::new(self.urlencoder, key); value.serialize(part::PartSerializer::new(value_sink)) } fn end(self) -> Result { Ok(self.urlencoder) } } impl<'input, 'output, Target> ser::SerializeStructVariant for StructVariantSerializer<'input, 'output, Target> where Target: UrlEncodedTarget, { type Ok = &'output mut UrlEncodedSerializer<'input, Target>; type Error = Error; fn serialize_field( &mut self, key: &'static str, value: &T, ) -> Result<(), Error> { self.inner.serialize_field(key, value) } fn end(self) -> Result { self.inner.end() } } impl Error { fn top_level() -> Self { let msg = "top-level serializer supports only maps and structs"; Error::Custom(msg.into()) } fn no_key() -> Self { let msg = "tried to serialize a value before serializing key"; Error::Custom(msg.into()) } } ruma-common-0.10.5/src/serde/urlencoded.rs000064400000000000000000000003061046102023000165230ustar 00000000000000//! `x-www-form-urlencoded` meets Serde pub mod de; pub mod ser; #[doc(inline)] pub use de::{from_bytes, from_reader, from_str, Deserializer}; #[doc(inline)] pub use ser::{to_string, Serializer}; ruma-common-0.10.5/src/serde.rs000064400000000000000000000037211046102023000144030ustar 00000000000000//! (De)serialization helpers for other Ruma crates. //! //! Part of that is a fork of [serde_urlencoded], with support for sequences in `Deserialize` / //! `Serialize` structs (e.g. `Vec`) that are (de)serialized as `field=val1&field=val2`. //! //! [serde_urlencoded]: https://github.com/nox/serde_urlencoded use serde::{de, Deserialize}; use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; pub mod base64; mod buf; pub mod can_be_empty; mod cow; pub mod duration; mod empty; pub mod json_string; mod raw; pub mod single_element_seq; mod strings; pub mod test; pub mod urlencoded; pub use self::{ base64::{Base64, Base64DecodeError}, buf::{json_to_buf, slice_to_buf}, can_be_empty::{is_empty, CanBeEmpty}, cow::deserialize_cow_str, empty::vec_as_map_of_empty, raw::Raw, strings::{ btreemap_deserialize_v1_powerlevel_values, deserialize_v1_powerlevel, empty_string_as_none, none_as_empty_string, }, }; /// The inner type of [`JsonValue::Object`]. pub type JsonObject = serde_json::Map; /// Check whether a value is equal to its default value. pub fn is_default(val: &T) -> bool { *val == T::default() } /// Simply returns `true`. /// /// Useful for `#[serde(default = ...)]`. pub fn default_true() -> bool { true } /// Simply dereferences the given bool. /// /// Useful for `#[serde(skip_serializing_if = ...)]`. #[allow(clippy::trivially_copy_pass_by_ref)] pub fn is_true(b: &bool) -> bool { *b } /// Helper function for `serde_json::value::RawValue` deserialization. pub fn from_raw_json_value<'a, T, E>(val: &'a RawJsonValue) -> Result where T: Deserialize<'a>, E: de::Error, { serde_json::from_str(val.get()).map_err(E::custom) } pub use ruma_macros::{ AsRefStr, DeserializeFromCowStr, DisplayAsRefStr, FromString, Incoming, OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, SerializeAsRefStr, StringEnum, _FakeDeriveSerde, }; ruma-common-0.10.5/src/thirdparty.rs000064400000000000000000000246441046102023000155020ustar 00000000000000//! Common types for the [third party networks module][thirdparty]. //! //! [thirdparty]: https://spec.matrix.org/v1.2/client-server-api/#third-party-networks use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::{ serde::StringEnum, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedUserId, PrivOwnedStr, }; /// Metadata about a third party protocol. /// /// To create an instance of this type, first create a `ProtocolInit` and convert it via /// `Protocol::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Protocol { /// Fields which may be used to identify a third party user. pub user_fields: Vec, /// Fields which may be used to identify a third party location. pub location_fields: Vec, /// A content URI representing an icon for the third party protocol. /// /// If you activate the `compat` feature, this field being absent in JSON will result in an /// empty string here during deserialization. #[cfg_attr(feature = "compat", serde(default))] pub icon: String, /// The type definitions for the fields defined in `user_fields` and `location_fields`. pub field_types: BTreeMap, /// A list of objects representing independent instances of configuration. pub instances: Vec, } /// Initial set of fields of `Protocol`. /// /// This struct will not be updated even if additional fields are added to `Prococol` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct ProtocolInit { /// Fields which may be used to identify a third party user. pub user_fields: Vec, /// Fields which may be used to identify a third party location. pub location_fields: Vec, /// A content URI representing an icon for the third party protocol. pub icon: String, /// The type definitions for the fields defined in `user_fields` and `location_fields`. pub field_types: BTreeMap, /// A list of objects representing independent instances of configuration. pub instances: Vec, } impl From for Protocol { fn from(init: ProtocolInit) -> Self { let ProtocolInit { user_fields, location_fields, icon, field_types, instances } = init; Self { user_fields, location_fields, icon, field_types, instances } } } /// Metadata about an instance of a third party protocol. /// /// To create an instance of this type, first create a `ProtocolInstanceInit` and convert it via /// `ProtocolInstance::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct ProtocolInstance { /// A human-readable description for the protocol, such as the name. pub desc: String, /// An optional content URI representing the protocol. #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, /// Preset values for `fields` the client may use to search by. pub fields: BTreeMap, /// A unique identifier across all instances. pub network_id: String, /// A unique identifier across all instances. /// /// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833). #[cfg(feature = "unstable-pre-spec")] pub instance_id: String, } /// Initial set of fields of `Protocol`. /// /// This struct will not be updated even if additional fields are added to `Prococol` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct ProtocolInstanceInit { /// A human-readable description for the protocol, such as the name. pub desc: String, /// Preset values for `fields` the client may use to search by. pub fields: BTreeMap, /// A unique identifier across all instances. pub network_id: String, /// A unique identifier across all instances. /// /// See [matrix-spec#833](https://github.com/matrix-org/matrix-spec/issues/833). #[cfg(feature = "unstable-pre-spec")] pub instance_id: String, } impl From for ProtocolInstance { fn from(init: ProtocolInstanceInit) -> Self { let ProtocolInstanceInit { desc, fields, network_id, #[cfg(feature = "unstable-pre-spec")] instance_id, } = init; Self { desc, icon: None, fields, network_id, #[cfg(feature = "unstable-pre-spec")] instance_id, } } } /// A type definition for a field used to identify third party users or locations. /// /// To create an instance of this type, first create a `FieldTypeInit` and convert it via /// `FieldType::from` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct FieldType { /// A regular expression for validation of a field's value. pub regexp: String, /// A placeholder serving as a valid example of the field value. pub placeholder: String, } /// Initial set of fields of `FieldType`. /// /// This struct will not be updated even if additional fields are added to `FieldType` in a new /// (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct FieldTypeInit { /// A regular expression for validation of a field's value. pub regexp: String, /// A placeholder serving as a valid example of the field value. pub placeholder: String, } impl From for FieldType { fn from(init: FieldTypeInit) -> Self { let FieldTypeInit { regexp, placeholder } = init; Self { regexp, placeholder } } } /// A third party network location. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Location { /// An alias for a matrix room. pub alias: OwnedRoomAliasId, /// The protocol ID that the third party location is a part of. pub protocol: String, /// Information used to identify this third party location. pub fields: BTreeMap, } impl Location { /// Creates a new `Location` with the given alias, protocol and fields. pub fn new( alias: OwnedRoomAliasId, protocol: String, fields: BTreeMap, ) -> Self { Self { alias, protocol, fields } } } /// A third party network user. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct User { /// A matrix user ID representing a third party user. pub userid: OwnedUserId, /// The protocol ID that the third party user is a part of. pub protocol: String, /// Information used to identify this third party user. pub fields: BTreeMap, } impl User { /// Creates a new `User` with the given userid, protocol and fields. pub fn new(userid: OwnedUserId, protocol: String, fields: BTreeMap) -> Self { Self { userid, protocol, fields } } } /// The medium of a third party identifier. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "lowercase")] #[non_exhaustive] pub enum Medium { /// Email address identifier Email, /// Phone number identifier Msisdn, #[doc(hidden)] _Custom(PrivOwnedStr), } /// An identifier external to Matrix. /// /// To create an instance of this type, first create a `ThirdPartyIdentifierInit` and convert it to /// this type using `ThirdPartyIdentifier::Init` / `.into()`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(test, derive(PartialEq))] pub struct ThirdPartyIdentifier { /// The third party identifier address. pub address: String, /// The medium of third party identifier. pub medium: Medium, /// The time when the identifier was validated by the identity server. pub validated_at: MilliSecondsSinceUnixEpoch, /// The time when the homeserver associated the third party identifier with the user. pub added_at: MilliSecondsSinceUnixEpoch, } /// Initial set of fields of `ThirdPartyIdentifier`. /// /// This struct will not be updated even if additional fields are added to `ThirdPartyIdentifier` /// in a new (non-breaking) release of the Matrix specification. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] pub struct ThirdPartyIdentifierInit { /// The third party identifier address. pub address: String, /// The medium of third party identifier. pub medium: Medium, /// The time when the identifier was validated by the identity server. pub validated_at: MilliSecondsSinceUnixEpoch, /// The time when the homeserver associated the third party identifier with the user. pub added_at: MilliSecondsSinceUnixEpoch, } impl From for ThirdPartyIdentifier { fn from(init: ThirdPartyIdentifierInit) -> Self { let ThirdPartyIdentifierInit { address, medium, validated_at, added_at } = init; ThirdPartyIdentifier { address, medium, validated_at, added_at } } } #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{Medium, ThirdPartyIdentifier}; use crate::MilliSecondsSinceUnixEpoch; #[test] fn third_party_identifier_serde() { let third_party_id = ThirdPartyIdentifier { address: "monkey@banana.island".into(), medium: Medium::Email, validated_at: MilliSecondsSinceUnixEpoch(1_535_176_800_000_u64.try_into().unwrap()), added_at: MilliSecondsSinceUnixEpoch(1_535_336_848_756_u64.try_into().unwrap()), }; let third_party_id_serialized = json!({ "medium": "email", "address": "monkey@banana.island", "validated_at": 1_535_176_800_000_u64, "added_at": 1_535_336_848_756_u64 }); assert_eq!(to_json_value(third_party_id.clone()).unwrap(), third_party_id_serialized); assert_eq!(third_party_id, from_json_value(third_party_id_serialized).unwrap()); } } ruma-common-0.10.5/src/time.rs000064400000000000000000000106571046102023000142450ustar 00000000000000use std::time::{Duration, SystemTime, UNIX_EPOCH}; use js_int::{uint, UInt}; use serde::{Deserialize, Serialize}; /// A timestamp represented as the number of milliseconds since the unix epoch. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] #[serde(transparent)] pub struct MilliSecondsSinceUnixEpoch(pub UInt); impl MilliSecondsSinceUnixEpoch { /// Creates a new `MilliSecondsSinceUnixEpoch` from the given `SystemTime`, if it is not before /// the unix epoch, or too large to be represented. pub fn from_system_time(time: SystemTime) -> Option { let duration = time.duration_since(UNIX_EPOCH).ok()?; let millis = duration.as_millis().try_into().ok()?; Some(Self(millis)) } /// The current system time in milliseconds since the unix epoch. pub fn now() -> Self { #[cfg(not(all(target_arch = "wasm32", target_os = "unknown", feature = "js")))] return Self::from_system_time(SystemTime::now()).expect("date out of range"); #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "js"))] return Self(f64_to_uint(js_sys::Date::now())); } /// Creates a new `SystemTime` from `self`, if it can be represented. pub fn to_system_time(self) -> Option { UNIX_EPOCH.checked_add(Duration::from_millis(self.0.into())) } /// Get the time since the unix epoch in milliseconds. pub fn get(&self) -> UInt { self.0 } /// Get time since the unix epoch in seconds. pub fn as_secs(&self) -> UInt { self.0 / uint!(1000) } } /// A timestamp represented as the number of seconds since the unix epoch. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] #[serde(transparent)] pub struct SecondsSinceUnixEpoch(pub UInt); impl SecondsSinceUnixEpoch { /// Creates a new `MilliSecondsSinceUnixEpoch` from the given `SystemTime`, if it is not before /// the unix epoch, or too large to be represented. pub fn from_system_time(time: SystemTime) -> Option { let duration = time.duration_since(UNIX_EPOCH).ok()?; let millis = duration.as_secs().try_into().ok()?; Some(Self(millis)) } /// The current system-time as seconds since the unix epoch. pub fn now() -> Self { #[cfg(not(all(target_arch = "wasm32", target_os = "unknown", feature = "js")))] return Self::from_system_time(SystemTime::now()).expect("date out of range"); #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "js"))] return Self(f64_to_uint(js_sys::Date::now() / 1000.0)); } /// Creates a new `SystemTime` from `self`, if it can be represented. pub fn to_system_time(self) -> Option { UNIX_EPOCH.checked_add(Duration::from_secs(self.0.into())) } /// Get time since the unix epoch in seconds. pub fn get(&self) -> UInt { self.0 } } #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "js"))] fn f64_to_uint(val: f64) -> UInt { // UInt::MAX milliseconds is ~285 616 years, we do not account for that // (or for dates before the unix epoch which would have to be negative) UInt::try_from(val as u64).expect("date out of range") } #[cfg(test)] mod tests { use std::time::{Duration, UNIX_EPOCH}; use js_int::uint; use serde::{Deserialize, Serialize}; use serde_json::json; use super::{MilliSecondsSinceUnixEpoch, SecondsSinceUnixEpoch}; #[derive(Clone, Debug, Deserialize, Serialize)] struct SystemTimeTest { millis: MilliSecondsSinceUnixEpoch, secs: SecondsSinceUnixEpoch, } #[test] fn deserialize() { let json = json!({ "millis": 3000, "secs": 60 }); let time = serde_json::from_value::(json).unwrap(); assert_eq!(time.millis.to_system_time(), Some(UNIX_EPOCH + Duration::from_millis(3000))); assert_eq!(time.secs.to_system_time(), Some(UNIX_EPOCH + Duration::from_secs(60))); } #[test] fn serialize() { let request = SystemTimeTest { millis: MilliSecondsSinceUnixEpoch::from_system_time(UNIX_EPOCH + Duration::new(2, 0)) .unwrap(), secs: SecondsSinceUnixEpoch(uint!(0)), }; assert_eq!(serde_json::to_value(&request).unwrap(), json!({ "millis": 2000, "secs": 0 })); } } ruma-common-0.10.5/src/to_device.rs000064400000000000000000000043671046102023000152510ustar 00000000000000//! Common types for the Send-To-Device Messaging //! //! [send-to-device]: https://spec.matrix.org/v1.2/client-server-api/#send-to-device-messaging use std::fmt::{Display, Formatter, Result as FmtResult}; use serde::{ de::{self, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; use crate::OwnedDeviceId; /// Represents one or all of a user's devices. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(clippy::exhaustive_enums)] pub enum DeviceIdOrAllDevices { /// Represents a device Id for one of a user's devices. DeviceId(OwnedDeviceId), /// Represents all devices for a user. AllDevices, } impl Display for DeviceIdOrAllDevices { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { DeviceIdOrAllDevices::DeviceId(device_id) => write!(f, "{device_id}"), DeviceIdOrAllDevices::AllDevices => write!(f, "*"), } } } impl From for DeviceIdOrAllDevices { fn from(d: OwnedDeviceId) -> Self { DeviceIdOrAllDevices::DeviceId(d) } } impl TryFrom<&str> for DeviceIdOrAllDevices { type Error = &'static str; fn try_from(device_id_or_all_devices: &str) -> Result { if device_id_or_all_devices.is_empty() { Err("Device identifier cannot be empty") } else if "*" == device_id_or_all_devices { Ok(DeviceIdOrAllDevices::AllDevices) } else { Ok(DeviceIdOrAllDevices::DeviceId(device_id_or_all_devices.into())) } } } impl Serialize for DeviceIdOrAllDevices { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Self::DeviceId(device_id) => device_id.serialize(serializer), Self::AllDevices => serializer.serialize_str("*"), } } } impl<'de> Deserialize<'de> for DeviceIdOrAllDevices { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = crate::serde::deserialize_cow_str(deserializer)?; DeviceIdOrAllDevices::try_from(s.as_ref()).map_err(|_| { de::Error::invalid_value(Unexpected::Str(&s), &"a valid device identifier or '*'") }) } } ruma-common-0.10.5/tests/api/conversions.rs000064400000000000000000000112551046102023000167760ustar 00000000000000#![allow(clippy::exhaustive_structs)] use ruma_common::{ api::{ ruma_api, IncomingRequest as _, MatrixVersion, OutgoingRequest as _, OutgoingRequestAppserviceExt, SendAccessToken, }, user_id, OwnedUserId, }; ruma_api! { metadata: { description: "Does something.", method: POST, name: "my_endpoint", unstable_path: "/_matrix/foo/:bar/:user", rate_limited: false, authentication: None, } request: { pub hello: String, #[ruma_api(header = CONTENT_TYPE)] pub world: String, #[ruma_api(query)] pub q1: String, #[ruma_api(query)] pub q2: u32, #[ruma_api(path)] pub bar: String, #[ruma_api(path)] pub user: OwnedUserId, } response: { pub hello: String, #[ruma_api(header = CONTENT_TYPE)] pub world: String, #[serde(skip_serializing_if = "Option::is_none")] pub optional_flag: Option, } } #[test] fn request_serde() { let req = Request { hello: "hi".to_owned(), world: "test".to_owned(), q1: "query_param_special_chars %/&@!".to_owned(), q2: 55, bar: "barVal".to_owned(), user: user_id!("@bazme:ruma.io").to_owned(), }; let http_req = req .clone() .try_into_http_request::>( "https://homeserver.tld", SendAccessToken::None, &[MatrixVersion::V1_1], ) .unwrap(); let req2 = Request::try_from_http_request(http_req, &["barVal", "@bazme:ruma.io"]).unwrap(); assert_eq!(req.hello, req2.hello); assert_eq!(req.world, req2.world); assert_eq!(req.q1, req2.q1); assert_eq!(req.q2, req2.q2); assert_eq!(req.bar, req2.bar); assert_eq!(req.user, req2.user); } #[test] fn invalid_uri_should_not_panic() { let req = Request { hello: "hi".to_owned(), world: "test".to_owned(), q1: "query_param_special_chars %/&@!".to_owned(), q2: 55, bar: "barVal".to_owned(), user: user_id!("@bazme:ruma.io").to_owned(), }; let result = req.try_into_http_request::>( "invalid uri", SendAccessToken::None, &[MatrixVersion::V1_1], ); result.unwrap_err(); } #[test] fn request_with_user_id_serde() { let req = Request { hello: "hi".to_owned(), world: "test".to_owned(), q1: "query_param_special_chars %/&@!".to_owned(), q2: 55, bar: "barVal".to_owned(), user: user_id!("@bazme:ruma.io").to_owned(), }; let user_id = user_id!("@_virtual_:ruma.io"); let http_req = req .try_into_http_request_with_user_id::>( "https://homeserver.tld", SendAccessToken::None, user_id, &[MatrixVersion::V1_1], ) .unwrap(); let query = http_req.uri().query().unwrap(); assert_eq!( query, "q1=query_param_special_chars+%25%2F%26%40%21&q2=55&user_id=%40_virtual_%3Aruma.io" ); } mod without_query { use ruma_common::{api::MatrixVersion, OwnedUserId}; use super::{ruma_api, user_id, OutgoingRequestAppserviceExt, SendAccessToken}; ruma_api! { metadata: { description: "Does something without query.", method: POST, name: "my_endpoint", unstable_path: "/_matrix/foo/:bar/:user", rate_limited: false, authentication: None, } request: { pub hello: String, #[ruma_api(header = CONTENT_TYPE)] pub world: String, #[ruma_api(path)] pub bar: String, #[ruma_api(path)] pub user: OwnedUserId, } response: { pub hello: String, #[ruma_api(header = CONTENT_TYPE)] pub world: String, #[serde(skip_serializing_if = "Option::is_none")] pub optional_flag: Option, } } #[test] fn request_without_query_with_user_id_serde() { let req = Request { hello: "hi".to_owned(), world: "test".to_owned(), bar: "barVal".to_owned(), user: user_id!("@bazme:ruma.io").to_owned(), }; let user_id = user_id!("@_virtual_:ruma.io"); let http_req = req .try_into_http_request_with_user_id::>( "https://homeserver.tld", SendAccessToken::None, user_id, &[MatrixVersion::V1_1], ) .unwrap(); let query = http_req.uri().query().unwrap(); assert_eq!(query, "user_id=%40_virtual_%3Aruma.io"); } } ruma-common-0.10.5/tests/api/header_override.rs000064400000000000000000000034151046102023000175540ustar 00000000000000#![allow(clippy::exhaustive_structs)] use http::header::{Entry, CONTENT_TYPE}; use ruma_common::api::{ ruma_api, MatrixVersion, OutgoingRequest as _, OutgoingResponse as _, SendAccessToken, }; ruma_api! { metadata: { description: "Does something.", method: GET, name: "no_fields", unstable_path: "/_matrix/my/endpoint", rate_limited: false, authentication: None, } request: { #[ruma_api(header = LOCATION)] pub location: Option, #[ruma_api(header = CONTENT_TYPE)] pub stuff: String, } response: { #[ruma_api(header = CONTENT_TYPE)] pub stuff: String, } } #[test] fn response_content_type_override() { let res = Response { stuff: "magic".into() }; let mut http_res = res.try_into_http_response::>().unwrap(); // Test that we correctly replaced the default content type, // not adding another content-type header. assert_eq!( match http_res.headers_mut().entry(CONTENT_TYPE) { Entry::Occupied(occ) => occ.iter().count(), _ => 0, }, 1 ); assert_eq!(http_res.headers().get("content-type").unwrap(), "magic"); } #[test] fn request_content_type_override() { let req = Request { location: None, stuff: "magic".into() }; let mut http_req = req .try_into_http_request::>( "https://homeserver.tld", SendAccessToken::None, &[MatrixVersion::V1_1], ) .unwrap(); assert_eq!( match http_req.headers_mut().entry(CONTENT_TYPE) { Entry::Occupied(occ) => occ.iter().count(), _ => 0, }, 1 ); assert_eq!(http_req.headers().get("content-type").unwrap(), "magic"); } ruma-common-0.10.5/tests/api/manual_endpoint_impl.rs000064400000000000000000000105101046102023000206150ustar 00000000000000//! PUT /_matrix/client/r0/directory/room/:room_alias #![allow(clippy::exhaustive_structs)] use bytes::BufMut; use http::{header::CONTENT_TYPE, method::Method}; use ruma_common::{ api::{ error::{ FromHttpRequestError, FromHttpResponseError, IntoHttpError, MatrixError, ServerError, }, AuthScheme, EndpointError, IncomingRequest, IncomingResponse, MatrixVersion, Metadata, OutgoingRequest, OutgoingResponse, SendAccessToken, }, OwnedRoomAliasId, OwnedRoomId, }; use serde::{Deserialize, Serialize}; /// A request to create a new room alias. #[derive(Debug, Clone)] pub struct Request { pub room_id: OwnedRoomId, // body pub room_alias: OwnedRoomAliasId, // path } const METADATA: Metadata = Metadata { description: "Add an alias to a room.", method: Method::PUT, name: "create_alias", unstable_path: Some("/_matrix/client/unstable/directory/room/:room_alias"), r0_path: Some("/_matrix/client/r0/directory/room/:room_alias"), stable_path: Some("/_matrix/client/v3/directory/room/:room_alias"), rate_limited: false, authentication: AuthScheme::None, added: Some(MatrixVersion::V1_0), deprecated: Some(MatrixVersion::V1_1), removed: Some(MatrixVersion::V1_2), }; impl OutgoingRequest for Request { type EndpointError = MatrixError; type IncomingResponse = Response; const METADATA: Metadata = METADATA; fn try_into_http_request( self, base_url: &str, _access_token: SendAccessToken<'_>, considering_versions: &'_ [MatrixVersion], ) -> Result, IntoHttpError> { let url = format!( "{}{}", base_url, ruma_common::api::select_path( considering_versions, &METADATA, Some(format_args!("/_matrix/client/unstable/directory/room/{}", self.room_alias)), Some(format_args!("/_matrix/client/r0/directory/room/{}", self.room_alias)), Some(format_args!("/_matrix/client/v3/directory/room/{}", self.room_alias)), )? ); let request_body = RequestBody { room_id: self.room_id }; let http_request = http::Request::builder() .method(METADATA.method) .uri(url) .body(ruma_common::serde::json_to_buf(&request_body)?) // this cannot fail because we don't give user-supplied data to any of the // builder methods .unwrap(); Ok(http_request) } } impl IncomingRequest for Request { type EndpointError = MatrixError; type OutgoingResponse = Response; const METADATA: Metadata = METADATA; fn try_from_http_request( request: http::Request, path_args: &[S], ) -> Result where B: AsRef<[u8]>, S: AsRef, { let (room_alias,) = serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::< _, serde::de::value::Error, >::new( path_args.iter().map(::std::convert::AsRef::as_ref), ))?; let request_body: RequestBody = serde_json::from_slice(request.body().as_ref())?; Ok(Request { room_id: request_body.room_id, room_alias }) } } #[derive(Debug, Serialize, Deserialize)] struct RequestBody { room_id: OwnedRoomId, } /// The response to a request to create a new room alias. #[derive(Clone, Copy, Debug)] pub struct Response; impl IncomingResponse for Response { type EndpointError = MatrixError; fn try_from_http_response>( http_response: http::Response, ) -> Result> { if http_response.status().as_u16() < 400 { Ok(Response) } else { Err(FromHttpResponseError::Server(ServerError::Known( ::try_from_http_response(http_response)?, ))) } } } impl OutgoingResponse for Response { fn try_into_http_response( self, ) -> Result, IntoHttpError> { let response = http::Response::builder() .header(CONTENT_TYPE, "application/json") .body(ruma_common::serde::slice_to_buf(b"{}")) .unwrap(); Ok(response) } } ruma-common-0.10.5/tests/api/mod.rs000064400000000000000000000003411046102023000151770ustar 00000000000000#![cfg(feature = "api")] mod conversions; mod header_override; mod manual_endpoint_impl; mod no_fields; mod optional_headers; mod path_arg_ordering; mod ruma_api; mod ruma_api_lifetime; mod ruma_api_macros; mod select_path; ruma-common-0.10.5/tests/api/no_fields.rs000064400000000000000000000040041046102023000163620ustar 00000000000000use ruma_common::api::{ MatrixVersion, OutgoingRequest as _, OutgoingResponse as _, SendAccessToken, }; mod get { ruma_common::api::ruma_api! { metadata: { description: "Does something.", method: GET, name: "no_fields", unstable_path: "/_matrix/my/endpoint", rate_limited: false, authentication: None, } request: {} response: {} } } mod post { ruma_common::api::ruma_api! { metadata: { description: "Does something.", method: POST, name: "no_fields", unstable_path: "/_matrix/my/endpoint", rate_limited: false, authentication: None, } request: {} response: {} } } #[test] fn empty_post_request_http_repr() { let req = post::Request {}; let http_req = req .try_into_http_request::>( "https://homeserver.tld", SendAccessToken::None, &[MatrixVersion::V1_1], ) .unwrap(); // Empty POST requests should contain an empty dictionary as a body... assert_eq!(http_req.body(), b"{}"); } #[test] fn empty_get_request_http_repr() { let req = get::Request {}; let http_req = req .try_into_http_request::>( "https://homeserver.tld", SendAccessToken::None, &[MatrixVersion::V1_1], ) .unwrap(); // ... but GET requests' bodies should be empty. assert_eq!(http_req.body().len(), 0); } #[test] fn empty_post_response_http_repr() { let res = post::Response {}; let http_res = res.try_into_http_response::>().unwrap(); // For the response, the body should be an empty dict again... assert_eq!(http_res.body(), b"{}"); } #[test] fn empty_get_response_http_repr() { let res = get::Response {}; let http_res = res.try_into_http_response::>().unwrap(); // ... even for GET requests. assert_eq!(http_res.body(), b"{}"); } ruma-common-0.10.5/tests/api/optional_headers.rs000064400000000000000000000007131046102023000177430ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Does something.", method: GET, name: "no_fields", unstable_path: "/_matrix/my/endpoint", rate_limited: false, authentication: None, } request: { #[ruma_api(header = LOCATION)] pub location: Option, } response: { #[ruma_api(header = LOCATION)] pub stuff: Option, } } ruma-common-0.10.5/tests/api/path_arg_ordering.rs000064400000000000000000000020531046102023000201000ustar 00000000000000use ruma_common::api::{ruma_api, IncomingRequest as _}; ruma_api! { metadata: { description: "Does something.", method: GET, name: "some_path_args", unstable_path: "/_matrix/:one/a/:two/b/:three/c", rate_limited: false, authentication: None, } request: { #[ruma_api(path)] pub three: String, #[ruma_api(path)] pub one: String, #[ruma_api(path)] pub two: String, } response: {} } #[test] fn path_ordering_is_correct() { let request = http::Request::builder() .method("GET") // This explicitly puts wrong values in the URI, as now we rely on the side-supplied // path_args slice, so this is just to ensure it *is* using that slice. .uri("https://www.rust-lang.org/_matrix/non/a/non/b/non/c") .body("") .unwrap(); let resp = Request::try_from_http_request(request, &["1", "2", "3"]).unwrap(); assert_eq!(resp.one, "1"); assert_eq!(resp.two, "2"); assert_eq!(resp.three, "3"); } ruma-common-0.10.5/tests/api/ruma_api.rs000064400000000000000000000010561046102023000162210ustar 00000000000000#[test] fn ui() { let t = trybuild::TestCases::new(); t.pass("tests/api/ui/01-api-sanity-check.rs"); t.compile_fail("tests/api/ui/02-invalid-path.rs"); t.pass("tests/api/ui/03-move-value.rs"); t.compile_fail("tests/api/ui/04-attributes.rs"); t.pass("tests/api/ui/05-request-only.rs"); t.pass("tests/api/ui/06-response-only.rs"); t.compile_fail("tests/api/ui/07-error-type-attribute.rs"); t.compile_fail("tests/api/ui/08-deprecated-without-added.rs"); t.compile_fail("tests/api/ui/09-removed-without-deprecated.rs"); } ruma-common-0.10.5/tests/api/ruma_api_lifetime.rs000064400000000000000000000102551046102023000201000ustar 00000000000000#![allow(clippy::exhaustive_structs)] #[derive(Copy, Clone, Debug, ruma_common::serde::Incoming, serde::Serialize)] pub struct OtherThing<'t> { pub some: &'t str, pub t: &'t [u8], } mod empty_response { use ruma_common::{api::ruma_api, RoomAliasId, RoomId}; ruma_api! { metadata: { description: "Add an alias to a room.", method: PUT, name: "create_alias", unstable_path: "/_matrix/client/r0/directory/room/:room_alias", rate_limited: false, authentication: AccessToken, } request: { /// The room alias to set. #[ruma_api(path)] pub room_alias: &'a RoomAliasId, /// The room ID to set. pub room_id: &'a RoomId, } response: {} } } mod nested_types { use ruma_common::{api::ruma_api, RoomAliasId}; ruma_api! { metadata: { description: "Add an alias to a room.", method: PUT, name: "create_alias", unstable_path: "/_matrix/client/r0/directory/room", rate_limited: false, authentication: AccessToken, } request: { /// The room alias to set. pub room_alias: &'a [Option<&'a RoomAliasId>], /// The room ID to set. pub room_id: &'b [Option>], } response: {} } } mod full_request_response { use ruma_common::api::ruma_api; use super::{IncomingOtherThing, OtherThing}; ruma_api! { metadata: { description: "Does something.", method: POST, name: "no_fields", unstable_path: "/_matrix/my/endpoint/:thing", rate_limited: false, authentication: None, } request: { #[ruma_api(query)] pub abc: &'a str, #[ruma_api(path)] pub thing: &'a str, #[ruma_api(header = CONTENT_TYPE)] pub stuff: &'a str, pub more: OtherThing<'t>, } response: { #[ruma_api(body)] pub thing: Vec, #[ruma_api(header = CONTENT_TYPE)] pub stuff: String, } } } mod full_request_response_with_query_map { use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Does something.", method: GET, name: "no_fields", unstable_path: "/_matrix/my/endpoint/:thing", rate_limited: false, authentication: None, } request: { #[ruma_api(query_map)] // pub abc: &'a [(&'a str, &'a str)], // TODO handle this use case pub abc: Vec<(String, String)>, #[ruma_api(path)] pub thing: &'a str, #[ruma_api(header = CONTENT_TYPE)] pub stuff: &'a str, } response: { #[ruma_api(body)] pub thing: String, #[ruma_api(header = CONTENT_TYPE)] pub stuff: String, } } } mod query_fields { use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Get the list of rooms in this homeserver's public directory.", method: GET, name: "get_public_rooms", unstable_path: "/_matrix/client/r0/publicRooms", rate_limited: false, authentication: None, } request: { /// Limit for the number of results to return. #[serde(skip_serializing_if = "Option::is_none")] #[ruma_api(query)] pub limit: Option, /// Pagination token from a previous request. #[serde(skip_serializing_if = "Option::is_none")] #[ruma_api(query)] pub since: Option<&'a str>, /// The server to fetch the public room lists from. /// /// `None` means the server this request is sent to. #[serde(skip_serializing_if = "Option::is_none")] #[ruma_api(query)] pub server: Option<&'a str>, } response: {} } } ruma-common-0.10.5/tests/api/ruma_api_macros.rs000064400000000000000000000074671046102023000176010ustar 00000000000000#![allow(clippy::exhaustive_structs)] pub mod some_endpoint { use ruma_common::{ api::ruma_api, events::{tag::TagEvent, AnyTimelineEvent}, serde::Raw, OwnedUserId, }; ruma_api! { metadata: { description: "Does something.", method: POST, // An `http::Method` constant. No imports required. name: "some_endpoint", unstable_path: "/_matrix/some/endpoint/:user", rate_limited: false, authentication: None, } request: { // With no attribute on the field, it will be put into the body of the request. pub a_field: String, // This value will be put into the "Content-Type" HTTP header. #[ruma_api(header = CONTENT_TYPE)] pub content_type: String, // This value will be put into the query string of the request's URL. #[ruma_api(query)] pub bar: String, // This value will be inserted into the request's URL in place of the // ":user" path component. #[ruma_api(path)] pub user: OwnedUserId, } response: { // This value will be extracted from the "Content-Type" HTTP header. #[ruma_api(header = CONTENT_TYPE)] pub content_type: String, // With no attribute on the field, it will be extracted from the body of the response. pub value: String, // You can use serde attributes on any kind of field #[serde(skip_serializing_if = "Option::is_none")] pub optional_flag: Option, // Use `Raw` instead of the actual event to allow additional fields to be sent... pub event: Raw, // ... and to allow unknown events when the endpoint deals with event collections. pub list_of_events: Vec>, } } } pub mod newtype_body_endpoint { use ruma_common::api::ruma_api; #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct MyCustomType { pub a_field: String, } ruma_api! { metadata: { description: "Does something.", method: PUT, name: "newtype_body_endpoint", unstable_path: "/_matrix/some/newtype/body/endpoint", rate_limited: false, authentication: None, } request: { #[ruma_api(body)] pub list_of_custom_things: Vec, } response: { #[ruma_api(body)] pub my_custom_thing: MyCustomType, } } } pub mod raw_body_endpoint { use ruma_common::api::ruma_api; #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct MyCustomType { pub a_field: String, } ruma_api! { metadata: { description: "Does something.", method: PUT, name: "newtype_body_endpoint", unstable_path: "/_matrix/some/newtype/body/endpoint", rate_limited: false, authentication: None, } request: { #[ruma_api(raw_body)] pub file: &'a [u8], } response: { #[ruma_api(raw_body)] pub file: Vec, } } } pub mod query_map_endpoint { use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Does something.", method: GET, name: "newtype_body_endpoint", unstable_path: "/_matrix/some/query/map/endpoint", rate_limited: false, authentication: None, } request: { #[ruma_api(query_map)] pub fields: Vec<(String, String)>, } response: {} } } ruma-common-0.10.5/tests/api/select_path.rs000064400000000000000000000045361046102023000167250ustar 00000000000000use assert_matches::assert_matches; use http::Method; use ruma_common::api::{ error::IntoHttpError, select_path, MatrixVersion::{V1_0, V1_1, V1_2}, Metadata, }; const BASE: Metadata = Metadata { description: "", method: Method::GET, name: "test_endpoint", unstable_path: Some("/unstable/path"), r0_path: Some("/r0/path"), stable_path: Some("/stable/path"), rate_limited: false, authentication: ruma_common::api::AuthScheme::None, added: None, deprecated: None, removed: None, }; const U: &str = "u"; const S: &str = "s"; const R: &str = "r"; // TODO add test that can hook into tracing and verify the deprecation warning is emitted #[test] fn select_stable() { let meta = Metadata { added: Some(V1_1), ..BASE }; let res = select_path(&[V1_0, V1_1], &meta, None, None, Some(format_args!("{S}"))) .unwrap() .to_string(); assert_eq!(res, S); } #[test] fn select_unstable() { let meta = BASE; let res = select_path(&[V1_0], &meta, Some(format_args!("{U}")), None, None).unwrap().to_string(); assert_eq!(res, U); } #[test] fn select_r0() { let meta = Metadata { added: Some(V1_0), ..BASE }; let res = select_path(&[V1_0], &meta, None, Some(format_args!("{R}")), Some(format_args!("{S}"))) .unwrap() .to_string(); assert_eq!(res, R); } #[test] fn select_removed_err() { let meta = Metadata { added: Some(V1_0), deprecated: Some(V1_1), removed: Some(V1_2), ..BASE }; let res = select_path( &[V1_2], &meta, Some(format_args!("{U}")), Some(format_args!("{R}")), Some(format_args!("{S}")), ) .unwrap_err(); assert_matches!(res, IntoHttpError::EndpointRemoved(V1_2)); } #[test] fn partially_removed_but_stable() { let meta = Metadata { added: Some(V1_0), deprecated: Some(V1_1), removed: Some(V1_2), ..BASE }; let res = select_path(&[V1_1], &meta, None, Some(format_args!("{R}")), Some(format_args!("{S}"))) .unwrap() .to_string(); assert_eq!(res, S); } #[test] fn no_unstable() { let meta = Metadata { added: Some(V1_1), ..BASE }; let res = select_path(&[V1_0], &meta, None, Some(format_args!("{R}")), Some(format_args!("{S}"))) .unwrap_err(); assert_matches!(res, IntoHttpError::NoUnstablePath); } ruma-common-0.10.5/tests/api/ui/01-api-sanity-check.rs000064400000000000000000000045041046102023000204110ustar 00000000000000use ruma_common::{ api::ruma_api, events::{tag::TagEvent, AnyTimelineEvent}, serde::Raw, }; ruma_api! { metadata: { description: "Does something.", method: POST, // An `http::Method` constant. No imports required. name: "some_endpoint", unstable_path: "/_matrix/some/msc1234/endpoint/:baz", r0_path: "/_matrix/some/r0/endpoint/:baz", stable_path: "/_matrix/some/v1/endpoint/:baz", rate_limited: false, authentication: None, added: 1.0, deprecated: 1.1, removed: 1.2, } request: { // With no attribute on the field, it will be put into the body of the request. pub foo: String, // This value will be put into the "Content-Type" HTTP header. #[ruma_api(header = CONTENT_TYPE)] pub content_type: String, // This value will be put into the query string of the request's URL. #[ruma_api(query)] pub bar: String, // This value will be inserted into the request's URL in place of the // ":baz" path component. #[ruma_api(path)] pub baz: String, } response: { // This value will be extracted from the "Content-Type" HTTP header. #[ruma_api(header = CONTENT_TYPE)] pub content_type: String, // With no attribute on the field, it will be extracted from the body of the response. pub value: String, // You can use serde attributes on any kind of field #[serde(skip_serializing_if = "Option::is_none")] pub optional_flag: Option, // Use `Raw` instead of the actual event to allow additional fields to be sent... pub event: Raw, // ... and to allow unknown events when the endpoint deals with event collections. pub list_of_events: Vec>, } } fn main() { use ruma_common::api::MatrixVersion; assert_eq!(METADATA.unstable_path, Some("/_matrix/some/msc1234/endpoint/:baz")); assert_eq!(METADATA.r0_path, Some("/_matrix/some/r0/endpoint/:baz")); assert_eq!(METADATA.stable_path, Some("/_matrix/some/v1/endpoint/:baz")); assert_eq!(METADATA.added, Some(MatrixVersion::V1_0)); assert_eq!(METADATA.deprecated, Some(MatrixVersion::V1_1)); assert_eq!(METADATA.removed, Some(MatrixVersion::V1_2)); } ruma-common-0.10.5/tests/api/ui/02-invalid-path.rs000064400000000000000000000013501046102023000176350ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "This will fail.", method: GET, name: "invalid_path", unstable_path: "µ/°/§/€", rate_limited: false, authentication: None, } request: { #[ruma_api(query_map)] pub fields: Vec<(String, String)>, } response: {} } ruma_api! { metadata: { description: "This will fail.", method: GET, name: "invalid_path", unstable_path: "path/to/invalid space/endpoint", rate_limited: false, authentication: None, } request: { #[ruma_api(query_map)] pub fields: Vec<(String, String)>, } response: {} } fn main() {} ruma-common-0.10.5/tests/api/ui/02-invalid-path.stderr000064400000000000000000000006511046102023000205170ustar 00000000000000error: path may only contain printable ASCII characters with no spaces --> $DIR/02-invalid-path.rs:8:24 | 8 | unstable_path: "µ/°/§/€", | ^^^^^^^^^ error: path may only contain printable ASCII characters with no spaces --> $DIR/02-invalid-path.rs:26:24 | 26 | unstable_path: "path/to/invalid space/endpoint", | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ruma-common-0.10.5/tests/api/ui/03-move-value.rs000064400000000000000000000051351046102023000173430ustar 00000000000000// This tests that the "body" fields are moved after all other fields because they // consume the request/response. mod newtype_body { use ruma_common::{api::ruma_api, OwnedUserId}; #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Foo; ruma_api! { metadata: { description: "Does something.", method: POST, name: "my_endpoint", unstable_path: "/_matrix/foo/:bar/", rate_limited: false, authentication: None, } request: { #[ruma_api(body)] pub q2: Foo, #[ruma_api(path)] pub bar: String, #[ruma_api(query)] pub baz: OwnedUserId, #[ruma_api(header = CONTENT_TYPE)] pub world: String, } response: { #[ruma_api(body)] pub q2: Foo, #[ruma_api(header = CONTENT_TYPE)] pub world: String, } } } mod raw_body { use ruma_common::{api::ruma_api, OwnedUserId}; ruma_api! { metadata: { description: "Does something.", method: POST, name: "my_endpoint", unstable_path: "/_matrix/foo/:bar/", rate_limited: false, authentication: None, } request: { #[ruma_api(raw_body)] pub q2: Vec, #[ruma_api(path)] pub bar: String, #[ruma_api(query)] pub baz: OwnedUserId, #[ruma_api(header = CONTENT_TYPE)] pub world: String, } response: { #[ruma_api(raw_body)] pub q2: Vec, #[ruma_api(header = CONTENT_TYPE)] pub world: String, } } } mod plain { use ruma_common::{api::ruma_api, OwnedUserId}; #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Foo; ruma_api! { metadata: { description: "Does something.", method: POST, name: "my_endpoint", unstable_path: "/_matrix/foo/:bar/", rate_limited: false, authentication: None, } request: { pub q2: Foo, #[ruma_api(path)] pub bar: String, #[ruma_api(query)] pub baz: OwnedUserId, #[ruma_api(header = CONTENT_TYPE)] pub world: String, } response: { pub q2: Vec, #[ruma_api(header = CONTENT_TYPE)] pub world: String, } } } fn main() {} ruma-common-0.10.5/tests/api/ui/04-attributes.rs000064400000000000000000000012221046102023000174430ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Does something.", method: POST, // An `http::Method` constant. No imports required. name: "some_endpoint", unstable_path: "/_matrix/some/endpoint/:baz", rate_limited: false, authentication: None, } #[not_a_real_attribute_should_fail] request: { pub foo: String, #[ruma_api(header = CONTENT_TYPE)] pub content_type: String, #[ruma_api(query)] pub bar: String, #[ruma_api(path)] pub baz: String, } response: { pub value: String, } } fn main() {} ruma-common-0.10.5/tests/api/ui/04-attributes.stderr000064400000000000000000000003151046102023000203240ustar 00000000000000error: cannot find attribute `not_a_real_attribute_should_fail` in this scope --> $DIR/04-attributes.rs:13:7 | 13 | #[not_a_real_attribute_should_fail] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ruma-common-0.10.5/tests/api/ui/05-request-only.rs000064400000000000000000000023151046102023000177310ustar 00000000000000use bytes::BufMut; use ruma_common::api::{ error::{FromHttpResponseError, IntoHttpError, MatrixError}, ruma_api, IncomingResponse, OutgoingResponse, }; ruma_api! { metadata: { description: "Does something.", method: POST, // An `http::Method` constant. No imports required. name: "some_endpoint", unstable_path: "/_matrix/some/endpoint/:foo", rate_limited: false, authentication: None, } #[derive(PartialEq)] // Make sure attributes work request: { // With no attribute on the field, it will be put into the body of the request. #[ruma_api(path)] pub foo: String, } } pub struct Response; impl IncomingResponse for Response { type EndpointError = MatrixError; fn try_from_http_response>( _: http::Response, ) -> Result> { todo!() } } impl OutgoingResponse for Response { fn try_into_http_response( self, ) -> Result, IntoHttpError> { todo!() } } fn main() { let req1 = Request { foo: "foo".into() }; let req2 = req1.clone(); assert_eq!(req1, req2); } ruma-common-0.10.5/tests/api/ui/06-response-only.rs000064400000000000000000000010361046102023000200770ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Does something.", method: POST, // An `http::Method` constant. No imports required. name: "some_endpoint", unstable_path: "/_matrix/some/endpoint/:baz", rate_limited: false, authentication: None, } #[derive(PartialEq)] // Make sure attributes work response: { pub flag: bool, } } fn main() { let res1 = Response { flag: false }; let res2 = res1.clone(); assert_eq!(res1, res2); } ruma-common-0.10.5/tests/api/ui/07-error-type-attribute.rs000064400000000000000000000007001046102023000213710ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "Does something.", method: POST, // An `http::Method` constant. No imports required. name: "some_endpoint", unstable_path: "/_matrix/some/endpoint/:baz", rate_limited: false, authentication: None, } request: {} response: {} #[derive(Default)] error: ruma_common::api::error::MatrixError } fn main() {} ruma-common-0.10.5/tests/api/ui/07-error-type-attribute.stderr000064400000000000000000000001571046102023000222560ustar 00000000000000error: unexpected token --> $DIR/07-error-type-attribute.rs:17:5 | 17 | #[derive(Default)] | ^ ruma-common-0.10.5/tests/api/ui/08-deprecated-without-added.rs000064400000000000000000000006331046102023000221260ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "This will fail.", method: GET, name: "invalid_versions", unstable_path: "/a/path", rate_limited: false, authentication: None, deprecated: 1.1, } request: { #[ruma_api(query_map)] pub fields: Vec<(String, String)>, } response: {} } fn main() {} ruma-common-0.10.5/tests/api/ui/08-deprecated-without-added.stderr000064400000000000000000000006361046102023000230100ustar 00000000000000error: deprecated version is defined while added version is not defined --> $DIR/08-deprecated-without-added.rs:3:1 | 3 | / ruma_api! { 4 | | metadata: { 5 | | description: "This will fail.", 6 | | method: GET, ... | 20 | | response: {} 21 | | } | |_^ | = note: this error originates in the macro `ruma_api` (in Nightly builds, run with -Z macro-backtrace for more info) ruma-common-0.10.5/tests/api/ui/09-removed-without-deprecated copy.rs000064400000000000000000000006171046102023000234640ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "This will fail.", method: GET, name: "invalid_versions", path: "/a/path", rate_limited: false, authentication: None, removed: 1.1, } request: { #[ruma_api(query_map)] pub fields: Vec<(String, String)>, } response: {} } fn main() {} ruma-common-0.10.5/tests/api/ui/09-removed-without-deprecated.rs000064400000000000000000000006301046102023000225240ustar 00000000000000use ruma_common::api::ruma_api; ruma_api! { metadata: { description: "This will fail.", method: GET, name: "invalid_versions", unstable_path: "/a/path", rate_limited: false, authentication: None, removed: 1.1, } request: { #[ruma_api(query_map)] pub fields: Vec<(String, String)>, } response: {} } fn main() {} ruma-common-0.10.5/tests/api/ui/09-removed-without-deprecated.stderr000064400000000000000000000006421046102023000234060ustar 00000000000000error: removed version is defined while deprecated version is not defined --> $DIR/09-removed-without-deprecated.rs:3:1 | 3 | / ruma_api! { 4 | | metadata: { 5 | | description: "This will fail.", 6 | | method: GET, ... | 20 | | response: {} 21 | | } | |_^ | = note: this error originates in the macro `ruma_api` (in Nightly builds, run with -Z macro-backtrace for more info) ruma-common-0.10.5/tests/events/audio.rs000064400000000000000000000332611046102023000162630ustar 00000000000000#![cfg(feature = "unstable-msc3246")] use std::time::Duration; use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ audio::{Amplitude, AudioContent, AudioEventContent, Waveform, WaveformError}, file::{EncryptedContentInit, FileContent, FileContentInfo}, message::MessageContent, room::{ message::{ AudioMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent, }, JsonWebKeyInit, MediaSource, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, mxc_uri, room_id, serde::{Base64, CanBeEmpty}, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn waveform_deserialization_pass() { let json_data = json!([ 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, ]); let waveform = from_json_value::(json_data).unwrap(); assert_eq!(waveform.amplitudes().len(), 52); } #[test] fn waveform_deserialization_not_enough() { let json_data = json!([]); let err = from_json_value::(json_data).unwrap_err(); assert!(err.is_data()); assert_eq!(err.to_string(), WaveformError::NotEnoughValues.to_string()); } #[test] fn waveform_deserialization_clamp_amplitude() { let json_data = json!([ 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, ]); let waveform = from_json_value::(json_data).unwrap(); assert!(waveform.amplitudes().iter().all(|amp| amp.get() == Amplitude::MAX.into())); } #[test] fn plain_content_serialization() { let event_content = AudioEventContent::plain( "Upload: my_sound.ogg", FileContent::plain(mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), None), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_sound.ogg", "m.file": { "url": "mxc://notareal.hs/abcdef", }, "m.audio": {} }) ); } #[test] fn encrypted_content_serialization() { let event_content = AudioEventContent::plain( "Upload: my_sound.ogg", FileContent::encrypted( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), EncryptedContentInit { key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), None, ), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_sound.ogg", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" }, "m.audio": {} }) ); } #[test] fn event_serialization() { let event = OriginalMessageLikeEvent { content: assign!( AudioEventContent::with_message( MessageContent::html( "Upload: my_mix.mp3", "Upload: my_mix.mp3", ), FileContent::plain( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), Some(Box::new(assign!( FileContentInfo::new(), { name: Some("my_mix.mp3".to_owned()), mimetype: Some("audio/mp3".to_owned()), size: Some(uint!(897_774)), } ))), ) ), { audio: assign!( AudioContent::new(), { duration: Some(Duration::from_secs(123)) } ), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), }), } ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.html": "Upload: my_mix.mp3", "org.matrix.msc1767.text": "Upload: my_mix.mp3", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_mix.mp3", "mimetype": "audio/mp3", "size": 897_774, }, "m.audio": { "duration": 123_000, }, "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com" } } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.audio", }) ); } #[test] fn plain_content_deserialization() { let json_data = json!({ "m.text": "Upload: my_new_song.webm", "m.file": { "url": "mxc://notareal.hs/abcdef", }, "m.audio": { "waveform": [ 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, 13, 34, 987, 937, 345, 648, 1, 366, 235, 125, 904, 783, 734, ], } }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Upload: my_new_song.webm")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); let waveform = content.audio.waveform.unwrap(); assert_eq!(waveform.amplitudes().len(), 52); } #[test] fn encrypted_content_deserialization() { let json_data = json!({ "m.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" }, "m.audio": {}, }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Upload: my_file.txt")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert!(content.file.encryption_info.is_some()); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "m.text": "Upload: airplane_sound.opus", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "airplane_sound.opus", "mimetype": "audio/opus", "size": 123_774, }, "m.audio": { "duration": 5_300, } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.audio", }); let message_event = assert_matches!( from_json_value::(json_data).unwrap(), AnyMessageLikeEvent::Audio(MessageLikeEvent::Original(message_event)) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); let content = message_event.content; assert_eq!(content.message.find_plain(), Some("Upload: airplane_sound.opus")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); let info = content.file.info.unwrap(); assert_eq!(info.name.as_deref(), Some("airplane_sound.opus")); assert_eq!(info.mimetype.as_deref(), Some("audio/opus")); assert_eq!(info.size, Some(uint!(123_774))); assert_eq!(content.audio.duration, Some(Duration::from_millis(5_300))); assert_matches!(content.audio.waveform, None); } #[test] fn room_message_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain( "Upload: my_song.mp3".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_song.mp3", "url": "mxc://notareal.hs/file", "msgtype": "m.audio", "org.matrix.msc1767.text": "Upload: my_song.mp3", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.audio": {}, }) ); } #[test] fn room_message_stable_deserialization() { let json_data = json!({ "body": "Upload: my_song.mp3", "url": "mxc://notareal.hs/file", "msgtype": "m.audio", "m.text": "Upload: my_song.mp3", "m.file": { "url": "mxc://notareal.hs/file", }, "m.audio": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Audio(content) => content); assert_eq!(content.body, "Upload: my_song.mp3"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_song.mp3"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } #[test] fn room_message_unstable_deserialization() { let json_data = json!({ "body": "Upload: my_song.mp3", "url": "mxc://notareal.hs/file", "msgtype": "m.audio", "org.matrix.msc1767.text": "Upload: my_song.mp3", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.audio": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Audio(content) => content); assert_eq!(content.body, "Upload: my_song.mp3"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_song.mp3"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } ruma-common-0.10.5/tests/events/call.rs000064400000000000000000000451311046102023000160740ustar 00000000000000#![cfg(feature = "unstable-msc2746")] use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ call::{ answer::CallAnswerEventContent, candidates::{CallCandidatesEventContent, Candidate}, hangup::{CallHangupEventContent, Reason}, invite::CallInviteEventContent, negotiate::CallNegotiateEventContent, reject::CallRejectEventContent, select_answer::CallSelectAnswerEventContent, AnswerSessionDescription, CallCapabilities, OfferSessionDescription, SessionDescription, SessionDescriptionType, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, room_id, user_id, MilliSecondsSinceUnixEpoch, VoipVersionId, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn invite_content_serialization() { let event_content = CallInviteEventContent::version_0( "abcdef".into(), uint!(30000), OfferSessionDescription::new("not a real sdp".to_owned()), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "call_id": "abcdef", "lifetime": 30000, "version": 0, "offer": { "type": "offer", "sdp": "not a real sdp", }, }) ); } #[test] fn invite_event_serialization() { let event = OriginalMessageLikeEvent { content: CallInviteEventContent::version_1( "abcdef".into(), "9876".into(), uint!(60000), OfferSessionDescription::new("not a real sdp".to_owned()), CallCapabilities::new(), ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "lifetime": 60000, "version": "1", "offer": { "type": "offer", "sdp": "not a real sdp", }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.invite", }) ); } #[test] fn invite_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "lifetime": 60000, "version": "1", "offer": { "type": "offer", "sdp": "not a real sdp", }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.invite", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallInvite(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id.unwrap(), "9876"); assert_eq!(content.lifetime, uint!(60000)); assert_eq!(content.version, VoipVersionId::V1); assert_eq!(content.offer.sdp, "not a real sdp"); assert!(!content.capabilities.dtmf); } #[test] fn answer_content_serialization() { let event_content = CallAnswerEventContent::version_0( AnswerSessionDescription::new("not a real sdp".to_owned()), "abcdef".into(), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "call_id": "abcdef", "version": 0, "answer": { "type": "answer", "sdp": "not a real sdp", }, }) ); } #[test] fn answer_event_serialization() { let event = OriginalMessageLikeEvent { content: CallAnswerEventContent::version_1( AnswerSessionDescription::new("not a real sdp".to_owned()), "abcdef".into(), "9876".into(), assign!(CallCapabilities::new(), { dtmf: true }), ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", "answer": { "type": "answer", "sdp": "not a real sdp", }, "capabilities": { "m.call.dtmf": true, }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.answer", }) ); } #[test] fn answer_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "org.matrix.1b", "answer": { "type": "answer", "sdp": "not a real sdp", }, "capabilities": { "m.call.dtmf": true, }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.answer", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallAnswer(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id.unwrap(), "9876"); assert_eq!(content.version.as_ref(), "org.matrix.1b"); assert_eq!(content.answer.sdp, "not a real sdp"); assert!(content.capabilities.dtmf); } #[test] fn candidates_content_serialization() { let event_content = CallCandidatesEventContent::version_0( "abcdef".into(), vec![Candidate::new("not a real candidate".to_owned(), "0".to_owned(), uint!(0))], ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "call_id": "abcdef", "version": 0, "candidates": [ { "candidate": "not a real candidate", "sdpMid": "0", "sdpMLineIndex": 0, }, ], }) ); } #[test] fn candidates_event_serialization() { let event = OriginalMessageLikeEvent { content: CallCandidatesEventContent::version_1( "abcdef".into(), "9876".into(), vec![ Candidate::new("not a real candidate".to_owned(), "0".to_owned(), uint!(0)), Candidate::new("another fake candidate".to_owned(), "0".to_owned(), uint!(1)), ], ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", "candidates": [ { "candidate": "not a real candidate", "sdpMid": "0", "sdpMLineIndex": 0, }, { "candidate": "another fake candidate", "sdpMid": "0", "sdpMLineIndex": 1, }, ], }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.candidates", }) ); } #[test] fn candidates_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", "candidates": [ { "candidate": "not a real candidate", "sdpMid": "0", "sdpMLineIndex": 0, }, { "candidate": "another fake candidate", "sdpMid": "0", "sdpMLineIndex": 1, }, ], }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.candidates", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallCandidates(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id.unwrap(), "9876"); assert_eq!(content.version, VoipVersionId::V1); assert_eq!(content.candidates.len(), 2); assert_eq!(content.candidates[0].candidate, "not a real candidate"); assert_eq!(content.candidates[0].sdp_mid, "0"); assert_eq!(content.candidates[0].sdp_m_line_index, uint!(0)); assert_eq!(content.candidates[1].candidate, "another fake candidate"); assert_eq!(content.candidates[1].sdp_mid, "0"); assert_eq!(content.candidates[1].sdp_m_line_index, uint!(1)); } #[test] fn hangup_content_serialization() { let event_content = CallHangupEventContent::version_0("abcdef".into()); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "call_id": "abcdef", "version": 0, "reason": "user_hangup", }) ); } #[test] fn hangup_event_serialization() { let event = OriginalMessageLikeEvent { content: CallHangupEventContent::version_1( "abcdef".into(), "9876".into(), Reason::IceFailed, ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", "reason": "ice_failed", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.hangup", }) ); } #[test] fn hangup_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.hangup", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallHangup(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id.unwrap(), "9876"); assert_eq!(content.version, VoipVersionId::V1); assert_eq!(content.reason, Some(Reason::UserHangup)); } #[test] fn negotiate_event_serialization() { let event = OriginalMessageLikeEvent { content: CallNegotiateEventContent::new( "abcdef".into(), "9876".into(), uint!(30000), SessionDescription::new(SessionDescriptionType::Offer, "not a real sdp".to_owned()), ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "lifetime": 30000, "description": { "type": "offer", "sdp": "not a real sdp", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.negotiate", }) ); } #[test] fn negotiate_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "lifetime": 30000, "description": { "type": "pranswer", "sdp": "not a real sdp", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.negotiate", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallNegotiate(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id, "9876"); assert_eq!(content.lifetime, uint!(30000)); assert_eq!(content.description.session_type, SessionDescriptionType::PrAnswer); assert_eq!(content.description.sdp, "not a real sdp"); } #[test] fn reject_event_serialization() { let event = OriginalMessageLikeEvent { content: CallRejectEventContent::version_1("abcdef".into(), "9876".into()), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.reject", }) ); } #[test] fn reject_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "version": "1", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.reject", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallReject(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id, "9876"); assert_eq!(content.version, VoipVersionId::V1); } #[test] fn select_answer_event_serialization() { let event = OriginalMessageLikeEvent { content: CallSelectAnswerEventContent::version_1( "abcdef".into(), "9876".into(), "6336".into(), ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "call_id": "abcdef", "party_id": "9876", "selected_party_id": "6336", "version": "1", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.select_answer", }) ); } #[test] fn select_answer_event_deserialization() { let json_data = json!({ "content": { "call_id": "abcdef", "party_id": "9876", "selected_party_id": "6336", "version": "1", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.call.select_answer", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::CallSelectAnswer(MessageLikeEvent::Original(message_event)) => message_event ); let content = message_event.content; assert_eq!(content.call_id, "abcdef"); assert_eq!(content.party_id, "9876"); assert_eq!(content.selected_party_id, "6336"); assert_eq!(content.version, VoipVersionId::V1); } ruma-common-0.10.5/tests/events/enums.rs000064400000000000000000000341451046102023000163130ustar 00000000000000use assert_matches::assert_matches; use js_int::{int, uint}; use ruma_common::{ event_id, events::{MessageLikeEvent, StateEvent, SyncMessageLikeEvent, SyncStateEvent}, room_alias_id, room_id, serde::test::serde_json_eq, user_id, }; use serde_json::{from_value as from_json_value, json, Value as JsonValue}; use ruma_common::{ events::{ room::{ aliases::RoomAliasesEventContent, message::{MessageType, RoomMessageEventContent}, power_levels::RoomPowerLevelsEventContent, }, AnyEphemeralRoomEvent, AnyMessageLikeEvent, AnyStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, EphemeralRoomEventType, GlobalAccountDataEventType, MessageLikeEventType, MessageLikeUnsigned, OriginalMessageLikeEvent, OriginalStateEvent, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent, RoomAccountDataEventType, StateEventType, ToDeviceEventType, }, MilliSecondsSinceUnixEpoch, }; fn message_event() -> JsonValue { json!({ "content": { "body": "baba", "format": "org.matrix.custom.html", "formatted_body": "baba", "msgtype": "m.text" }, "event_id": "$152037280074GZeOm:localhost", "origin_server_ts": 1, "sender": "@example:localhost", "room_id": "!room:room.com", "type": "m.room.message", "unsigned": { "age": 1 } }) } fn message_event_sync() -> JsonValue { json!({ "content": { "body": "baba", "format": "org.matrix.custom.html", "formatted_body": "baba", "msgtype": "m.text" }, "event_id": "$152037280074GZeOm:localhost", "origin_server_ts": 1, "sender": "@example:localhost", "type": "m.room.message", "unsigned": { "age": 1 } }) } fn aliases_event() -> JsonValue { json!({ "content": { "aliases": ["#somewhere:localhost"] }, "event_id": "$152037280074GZeOm:localhost", "origin_server_ts": 1, "sender": "@example:localhost", "state_key": "room.com", "room_id": "!room:room.com", "type": "m.room.aliases", "unsigned": { "age": 1 } }) } fn aliases_event_sync() -> JsonValue { json!({ "content": { "aliases": ["#somewhere:localhost"] }, "event_id": "$152037280074GZeOm:localhost", "origin_server_ts": 1, "sender": "@example:localhost", "state_key": "example.com", "type": "m.room.aliases", "unsigned": { "age": 1 } }) } #[test] fn power_event_sync_deserialization() { let json_data = json!({ "content": { "ban": 50, "events": { "m.room.avatar": 50, "m.room.canonical_alias": 50, "m.room.history_visibility": 100, "m.room.name": 50, "m.room.power_levels": 100 }, "events_default": 0, "invite": 0, "kick": 50, "redact": 50, "state_default": 50, "users": { "@example:localhost": 100 }, "users_default": 0 }, "event_id": "$15139375512JaHAW:localhost", "origin_server_ts": 45, "sender": "@example:localhost", "state_key": "", "type": "m.room.power_levels", "unsigned": { "age": 45 } }); let ban = assert_matches!( from_json_value::(json_data), Ok(AnySyncTimelineEvent::State( AnySyncStateEvent::RoomPowerLevels(SyncStateEvent::Original( OriginalSyncStateEvent { content: RoomPowerLevelsEventContent { ban, .. }, .. }, )), )) => ban ); assert_eq!(ban, int!(50)); } #[test] fn message_event_sync_deserialization() { let json_data = message_event_sync(); let text_content = assert_matches!( from_json_value::(json_data), Ok(AnySyncTimelineEvent::MessageLike( AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original( OriginalSyncMessageLikeEvent { content: RoomMessageEventContent { msgtype: MessageType::Text(text_content), .. }, .. }, )) )) => text_content ); assert_eq!(text_content.body, "baba"); let formatted = text_content.formatted.unwrap(); assert_eq!(formatted.body, "baba"); } #[test] fn aliases_event_sync_deserialization() { let json_data = aliases_event_sync(); let ev = assert_matches!( from_json_value::(json_data), Ok(AnySyncTimelineEvent::State(AnySyncStateEvent::RoomAliases(SyncStateEvent::Original( ev, )))) => ev ); assert_eq!(ev.content.aliases, vec![room_alias_id!("#somewhere:localhost")]); } #[test] fn message_room_event_deserialization() { let json_data = message_event(); let text_content = assert_matches!( from_json_value::(json_data), Ok(AnyTimelineEvent::MessageLike( AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original( OriginalMessageLikeEvent { content: RoomMessageEventContent { msgtype: MessageType::Text(text_content), .. }, .. }, )) )) => text_content ); assert_eq!(text_content.body, "baba"); let formatted = text_content.formatted.unwrap(); assert_eq!(formatted.body, "baba"); } #[test] fn message_event_serialization() { let event = OriginalMessageLikeEvent { content: RoomMessageEventContent::text_plain("test"), event_id: event_id!("$1234:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)), room_id: room_id!("!roomid:example.com").to_owned(), sender: user_id!("@test:example.com").to_owned(), unsigned: MessageLikeUnsigned::default(), }; #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( serde_json::to_string(&event).expect("Failed to serialize message event"), r#"{"type":"m.room.message","content":{"msgtype":"m.text","body":"test"},"event_id":"$1234:example.com","sender":"@test:example.com","origin_server_ts":0,"room_id":"!roomid:example.com"}"# ); #[cfg(feature = "unstable-msc1767")] assert_eq!( serde_json::to_string(&event).expect("Failed to serialize message event"), r#"{"type":"m.room.message","content":{"msgtype":"m.text","body":"test","org.matrix.msc1767.text":"test"},"event_id":"$1234:example.com","sender":"@test:example.com","origin_server_ts":0,"room_id":"!roomid:example.com"}"# ); } #[test] fn alias_room_event_deserialization() { let json_data = aliases_event(); let aliases = assert_matches!( from_json_value::(json_data), Ok(AnyTimelineEvent::State( AnyStateEvent::RoomAliases(StateEvent::Original(OriginalStateEvent { content: RoomAliasesEventContent { aliases, .. }, .. })) )) => aliases ); assert_eq!(aliases, vec![room_alias_id!("#somewhere:localhost")]); } #[test] fn message_event_deserialization() { let json_data = message_event(); let text_content = assert_matches!( from_json_value::(json_data), Ok(AnyTimelineEvent::MessageLike( AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(OriginalMessageLikeEvent { content: RoomMessageEventContent { msgtype: MessageType::Text(text_content), .. }, .. })) )) => text_content ); assert_eq!(text_content.body, "baba"); let formatted = text_content.formatted.unwrap(); assert_eq!(formatted.body, "baba"); } #[test] fn alias_event_deserialization() { let json_data = aliases_event(); let aliases = assert_matches!( from_json_value::(json_data), Ok(AnyTimelineEvent::State( AnyStateEvent::RoomAliases(StateEvent::Original(OriginalStateEvent { content: RoomAliasesEventContent { aliases, .. }, .. })) )) => aliases ); assert_eq!(aliases, vec![room_alias_id!("#somewhere:localhost")]); } #[test] fn alias_event_field_access() { let json_data = aliases_event(); let state_event = assert_matches!( from_json_value::(json_data.clone()), Ok(AnyTimelineEvent::State(state_event)) => state_event ); assert_eq!(state_event.state_key(), "room.com"); assert_eq!(state_event.room_id(), "!room:room.com"); assert_eq!(state_event.event_id(), "$152037280074GZeOm:localhost"); assert_eq!(state_event.sender(), "@example:localhost"); let deser = from_json_value::(json_data).unwrap(); let ev = assert_matches!(&deser, AnyStateEvent::RoomAliases(StateEvent::Original(ev)) => ev); assert_eq!(ev.content.aliases, vec![room_alias_id!("#somewhere:localhost")]); assert_eq!(deser.event_type().to_string(), "m.room.aliases"); } #[test] fn ephemeral_event_deserialization() { let json_data = json!({ "content": { "user_ids": [ "@alice:matrix.org", "@bob:example.com" ] }, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "type": "m.typing" }); let ephem = assert_matches!( from_json_value::(json_data), Ok(ephem @ AnyEphemeralRoomEvent::Typing(_)) => ephem ); assert_eq!(ephem.room_id(), "!jEsUZKDJdhlrceRyVU:example.org"); } #[test] #[allow(deprecated)] fn serialize_and_deserialize_from_display_form() { use ruma_common::events::EventType; serde_json_eq(EventType::CallAnswer, json!("m.call.answer")); serde_json_eq(MessageLikeEventType::CallAnswer, json!("m.call.answer")); serde_json_eq(EventType::CallCandidates, json!("m.call.candidates")); serde_json_eq(EventType::CallHangup, json!("m.call.hangup")); serde_json_eq(EventType::CallInvite, json!("m.call.invite")); serde_json_eq(EventType::Direct, json!("m.direct")); serde_json_eq(GlobalAccountDataEventType::Direct, json!("m.direct")); serde_json_eq(EventType::Dummy, json!("m.dummy")); serde_json_eq(EventType::ForwardedRoomKey, json!("m.forwarded_room_key")); serde_json_eq(EventType::FullyRead, json!("m.fully_read")); serde_json_eq(RoomAccountDataEventType::FullyRead, json!("m.fully_read")); serde_json_eq(EventType::KeyVerificationAccept, json!("m.key.verification.accept")); serde_json_eq(EventType::KeyVerificationCancel, json!("m.key.verification.cancel")); serde_json_eq(EventType::KeyVerificationDone, json!("m.key.verification.done")); serde_json_eq(EventType::KeyVerificationKey, json!("m.key.verification.key")); serde_json_eq(ToDeviceEventType::KeyVerificationKey, json!("m.key.verification.key")); serde_json_eq(EventType::KeyVerificationMac, json!("m.key.verification.mac")); serde_json_eq(EventType::KeyVerificationReady, json!("m.key.verification.ready")); serde_json_eq(EventType::KeyVerificationRequest, json!("m.key.verification.request")); serde_json_eq(EventType::KeyVerificationStart, json!("m.key.verification.start")); serde_json_eq(EventType::IgnoredUserList, json!("m.ignored_user_list")); serde_json_eq(EventType::PolicyRuleRoom, json!("m.policy.rule.room")); serde_json_eq(EventType::PolicyRuleServer, json!("m.policy.rule.server")); serde_json_eq(EventType::PolicyRuleUser, json!("m.policy.rule.user")); serde_json_eq(EventType::Presence, json!("m.presence")); serde_json_eq(EventType::PushRules, json!("m.push_rules")); serde_json_eq(EventType::Receipt, json!("m.receipt")); serde_json_eq(EventType::RoomAliases, json!("m.room.aliases")); serde_json_eq(EventType::RoomAvatar, json!("m.room.avatar")); serde_json_eq(EventType::RoomCanonicalAlias, json!("m.room.canonical_alias")); serde_json_eq(EventType::RoomCreate, json!("m.room.create")); serde_json_eq(StateEventType::RoomCreate, json!("m.room.create")); serde_json_eq(EventType::RoomEncrypted, json!("m.room.encrypted")); serde_json_eq(EventType::RoomEncryption, json!("m.room.encryption")); serde_json_eq(EventType::RoomGuestAccess, json!("m.room.guest_access")); serde_json_eq(EventType::RoomHistoryVisibility, json!("m.room.history_visibility")); serde_json_eq(EventType::RoomJoinRules, json!("m.room.join_rules")); serde_json_eq(EventType::RoomMember, json!("m.room.member")); serde_json_eq(EventType::RoomMessage, json!("m.room.message")); serde_json_eq(EventType::RoomName, json!("m.room.name")); serde_json_eq(EventType::RoomPinnedEvents, json!("m.room.pinned_events")); serde_json_eq(EventType::RoomPowerLevels, json!("m.room.power_levels")); serde_json_eq(EventType::RoomRedaction, json!("m.room.redaction")); serde_json_eq(EventType::RoomServerAcl, json!("m.room.server_acl")); serde_json_eq(EventType::RoomThirdPartyInvite, json!("m.room.third_party_invite")); serde_json_eq(EventType::RoomTombstone, json!("m.room.tombstone")); serde_json_eq(EventType::RoomTopic, json!("m.room.topic")); serde_json_eq(EventType::RoomKey, json!("m.room_key")); serde_json_eq(EventType::RoomKeyRequest, json!("m.room_key_request")); serde_json_eq(EventType::Sticker, json!("m.sticker")); serde_json_eq(EventType::Tag, json!("m.tag")); serde_json_eq(EventType::Typing, json!("m.typing")); serde_json_eq(EphemeralRoomEventType::Typing, json!("m.typing")); } ruma-common-0.10.5/tests/events/ephemeral_event.rs000064400000000000000000000066501046102023000203270ustar 00000000000000use assert_matches::assert_matches; use js_int::uint; use maplit::btreemap; use ruma_common::{ event_id, events::receipt::ReceiptType, room_id, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use ruma_common::events::{ receipt::{Receipt, ReceiptEventContent}, typing::TypingEventContent, AnyEphemeralRoomEvent, EphemeralRoomEvent, }; #[test] fn ephemeral_serialize_typing() { let aliases_event = EphemeralRoomEvent { content: TypingEventContent::new(vec![user_id!("@carl:example.com").to_owned()]), room_id: room_id!("!roomid:room.com").to_owned(), }; let actual = to_json_value(&aliases_event).unwrap(); let expected = json!({ "content": { "user_ids": [ "@carl:example.com" ] }, "room_id": "!roomid:room.com", "type": "m.typing", }); assert_eq!(actual, expected); } #[test] fn deserialize_ephemeral_typing() { let json_data = json!({ "content": { "user_ids": [ "@carl:example.com" ] }, "room_id": "!roomid:room.com", "type": "m.typing" }); let typing_event = assert_matches!( from_json_value::(json_data), Ok(AnyEphemeralRoomEvent::Typing(typing_event)) => typing_event ); assert_eq!(typing_event.content.user_ids.len(), 1); assert_eq!(typing_event.content.user_ids[0], "@carl:example.com"); assert_eq!(typing_event.room_id, "!roomid:room.com"); } #[test] fn ephemeral_serialize_receipt() { let event_id = event_id!("$h29iv0s8:example.com").to_owned(); let user_id = user_id!("@carl:example.com").to_owned(); let aliases_event = EphemeralRoomEvent { content: ReceiptEventContent(btreemap! { event_id => btreemap! { ReceiptType::Read => btreemap! { user_id => Receipt::new(MilliSecondsSinceUnixEpoch(uint!(1))), }, }, }), room_id: room_id!("!roomid:room.com").to_owned(), }; let actual = to_json_value(&aliases_event).unwrap(); let expected = json!({ "content": { "$h29iv0s8:example.com": { "m.read": { "@carl:example.com": { "ts": 1 } } } }, "room_id": "!roomid:room.com", "type": "m.receipt" }); assert_eq!(actual, expected); } #[test] fn deserialize_ephemeral_receipt() { let event_id = event_id!("$h29iv0s8:example.com"); let user_id = user_id!("@carl:example.com"); let json_data = json!({ "content": { "$h29iv0s8:example.com": { "m.read": { "@carl:example.com": { "ts": 1 } } } }, "room_id": "!roomid:room.com", "type": "m.receipt" }); let receipt_event = assert_matches!( from_json_value::(json_data), Ok(AnyEphemeralRoomEvent::Receipt(receipt_event)) => receipt_event ); let receipts = receipt_event.content.0; assert_eq!(receipts.len(), 1); assert_eq!(receipt_event.room_id, "!roomid:room.com"); let event_receipts = receipts.get(event_id).unwrap(); let type_receipts = event_receipts.get(&ReceiptType::Read).unwrap(); let user_receipt = type_receipts.get(user_id).unwrap(); assert_eq!(user_receipt.ts, Some(MilliSecondsSinceUnixEpoch(uint!(1)))); } ruma-common-0.10.5/tests/events/event.rs000064400000000000000000000005741046102023000163040ustar 00000000000000#[test] fn ui() { let t = trybuild::TestCases::new(); // rustc overflows when compiling this see: // https://github.com/rust-lang/rust/issues/55779 // there is a workaround in the file. t.pass("tests/events/ui/04-event-sanity-check.rs"); t.compile_fail("tests/events/ui/05-named-fields.rs"); t.compile_fail("tests/events/ui/06-no-content-field.rs"); } ruma-common-0.10.5/tests/events/event_content.rs000064400000000000000000000004471046102023000200350ustar 00000000000000#[test] fn ui() { let t = trybuild::TestCases::new(); t.pass("tests/events/ui/01-content-sanity-check.rs"); t.compile_fail("tests/events/ui/02-no-event-type.rs"); t.compile_fail("tests/events/ui/03-invalid-event-type.rs"); t.pass("tests/events/ui/10-content-wildcard.rs"); } ruma-common-0.10.5/tests/events/event_enums.rs000064400000000000000000000032111046102023000175020ustar 00000000000000use assert_matches::assert_matches; use js_int::uint; use ruma_common::{ events::{AnyMessageLikeEvent, MessageLikeEvent}, serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, VoipVersionId, }; use serde_json::{from_value as from_json_value, json}; #[test] fn ui() { let t = trybuild::TestCases::new(); t.pass("tests/events/ui/07-enum-sanity-check.rs"); t.compile_fail("tests/events/ui/08-enum-invalid-path.rs"); t.compile_fail("tests/events/ui/09-enum-invalid-kind.rs"); } #[test] fn deserialize_message_event() { let json_data = json!({ "content": { "answer": { "type": "answer", "sdp": "Hello" }, "call_id": "foofoo", "version": 0 }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "type": "m.call.answer" }); let message_event = assert_matches!( from_json_value::(json_data).unwrap(), AnyMessageLikeEvent::CallAnswer(MessageLikeEvent::Original(message_event)) => message_event ); assert_eq!(message_event.event_id, "$h29iv0s8:example.com"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(message_event.room_id, "!roomid:room.com"); assert_eq!(message_event.sender, "@carl:example.com"); assert!(message_event.unsigned.is_empty()); let content = message_event.content; assert_eq!(content.answer.sdp, "Hello"); assert_eq!(content.call_id, "foofoo"); assert_eq!(content.version, VoipVersionId::V0); } ruma-common-0.10.5/tests/events/file.rs000064400000000000000000000411231046102023000160750ustar 00000000000000#![cfg(feature = "unstable-msc3551")] use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ file::{EncryptedContentInit, FileContentInfo, FileEventContent}, message::MessageContent, room::{ message::{ FileMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent, }, EncryptedFileInit, JsonWebKeyInit, MediaSource, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, mxc_uri, room_id, serde::{Base64, CanBeEmpty}, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn plain_content_serialization() { let event_content = FileEventContent::plain( "Upload: my_file.txt", mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), None, ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/abcdef", } }) ); } #[test] fn encrypted_content_serialization() { let event_content = FileEventContent::encrypted( "Upload: my_file.txt", mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), EncryptedContentInit { key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), None, ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" } }) ); } #[test] fn file_event_serialization() { let event = OriginalMessageLikeEvent { content: assign!( FileEventContent::plain_message( MessageContent::html( "Upload: my_file.txt", "Upload: my_file.txt", ), mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), Some(Box::new(assign!( FileContentInfo::new(), { name: Some("my_file.txt".to_owned()), mimetype: Some("text/plain".to_owned()), size: Some(uint!(774)), } ))), ), { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), }), } ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.html": "Upload: my_file.txt", "org.matrix.msc1767.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_file.txt", "mimetype": "text/plain", "size": 774, }, "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com" } } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.file", }) ); } #[test] fn plain_content_deserialization() { let json_data = json!({ "m.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/abcdef", } }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Upload: my_file.txt")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); } #[test] fn encrypted_content_deserialization() { let json_data = json!({ "m.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" } }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Upload: my_file.txt")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert!(content.file.encryption_info.is_some()); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "m.message": [ { "body": "Upload: my_file.txt", "mimetype": "text/html"}, { "body": "Upload: my_file.txt", "mimetype": "text/plain"}, ], "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_file.txt", "mimetype": "text/plain", "size": 774, }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.file", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::File(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); let content = message_event.content; assert_eq!(content.message.find_plain(), Some("Upload: my_file.txt")); assert_eq!(content.message.find_html(), Some("Upload: my_file.txt")); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); let info = content.file.info.unwrap(); assert_eq!(info.name.as_deref(), Some("my_file.txt")); assert_eq!(info.mimetype.as_deref(), Some("text/plain")); assert_eq!(info.size, Some(uint!(774))); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); } #[test] fn room_message_plain_content_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::plain( "Upload: my_file.txt".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_file.txt", "url": "mxc://notareal.hs/file", "msgtype": "m.file", "org.matrix.msc1767.text": "Upload: my_file.txt", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, }) ); } #[test] fn room_message_encrypted_content_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::encrypted( "Upload: my_file.txt".to_owned(), EncryptedFileInit { url: mxc_uri!("mxc://notareal.hs/file").to_owned(), key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_file.txt", "file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, "msgtype": "m.file", "org.matrix.msc1767.text": "Upload: my_file.txt", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, }) ); } #[test] fn room_message_plain_content_stable_deserialization() { let json_data = json!({ "body": "Upload: my_file.txt", "url": "mxc://notareal.hs/file", "msgtype": "m.file", "m.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/file", }, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::File(content) => content); assert_eq!(content.body, "Upload: my_file.txt"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_file.txt"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } #[test] fn room_message_plain_content_unstable_deserialization() { let json_data = json!({ "body": "Upload: my_file.txt", "url": "mxc://notareal.hs/file", "msgtype": "m.file", "org.matrix.msc1767.text": "Upload: my_file.txt", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::File(content) => content); assert_eq!(content.body, "Upload: my_file.txt"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_file.txt"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } #[test] fn room_message_encrypted_content_stable_deserialization() { let json_data = json!({ "body": "Upload: my_file.txt", "file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, "msgtype": "m.file", "m.text": "Upload: my_file.txt", "m.file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::File(content) => content); assert_eq!(content.body, "Upload: my_file.txt"); let encrypted_file = assert_matches!(content.source, MediaSource::Encrypted(f) => f); assert_eq!(encrypted_file.url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_file.txt"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(file.is_encrypted()); } #[test] fn room_message_encrypted_content_unstable_deserialization() { let json_data = json!({ "body": "Upload: my_file.txt", "file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, "msgtype": "m.file", "org.matrix.msc1767.text": "Upload: my_file.txt", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::File(content) => content); assert_eq!(content.body, "Upload: my_file.txt"); let encrypted_file = assert_matches!(content.source, MediaSource::Encrypted(f) => f); assert_eq!(encrypted_file.url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_file.txt"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(file.is_encrypted()); } ruma-common-0.10.5/tests/events/image.rs000064400000000000000000000322051046102023000162410ustar 00000000000000#![cfg(feature = "unstable-msc3552")] use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ file::{EncryptedContentInit, FileContent, FileContentInfo}, image::{ ImageContent, ImageEventContent, ThumbnailContent, ThumbnailFileContent, ThumbnailFileContentInfo, }, message::MessageContent, room::{ message::{ ImageMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent, }, JsonWebKeyInit, MediaSource, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, mxc_uri, room_id, serde::{Base64, CanBeEmpty}, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn plain_content_serialization() { let event_content = ImageEventContent::plain( "Upload: my_image.jpg", FileContent::plain(mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), None), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_image.jpg", "m.file": { "url": "mxc://notareal.hs/abcdef", }, "m.image": {} }) ); } #[test] fn encrypted_content_serialization() { let event_content = ImageEventContent::plain( "Upload: my_image.jpg", FileContent::encrypted( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), EncryptedContentInit { key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), None, ), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_image.jpg", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" }, "m.image": {} }) ); } #[test] fn image_event_serialization() { let event = OriginalMessageLikeEvent { content: assign!( ImageEventContent::with_message( MessageContent::html( "Upload: my_house.jpg", "Upload: my_house.jpg", ), FileContent::plain( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), Some(Box::new(assign!( FileContentInfo::new(), { name: Some("my_house.jpg".to_owned()), mimetype: Some("image/jpeg".to_owned()), size: Some(uint!(897_774)), } ))), ) ), { image: Box::new(ImageContent::with_size(uint!(1920), uint!(1080))), thumbnail: vec![ThumbnailContent::new( ThumbnailFileContent::plain( mxc_uri!("mxc://notareal.hs/thumbnail").to_owned(), Some(Box::new(assign!(ThumbnailFileContentInfo::new(), { mimetype: Some("image/jpeg".to_owned()), size: Some(uint!(334_593)), }))) ), None )], caption: Some(MessageContent::plain("This is my house")), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), }), } ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.html": "Upload: my_house.jpg", "org.matrix.msc1767.text": "Upload: my_house.jpg", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_house.jpg", "mimetype": "image/jpeg", "size": 897_774, }, "m.image": { "width": 1920, "height": 1080, }, "m.thumbnail": [ { "url": "mxc://notareal.hs/thumbnail", "mimetype": "image/jpeg", "size": 334_593, } ], "m.caption": [ { "body": "This is my house", "mimetype": "text/plain", } ], "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com" } } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.image", }) ); } #[test] fn plain_content_deserialization() { let json_data = json!({ "m.text": "Upload: my_cat.png", "m.file": { "url": "mxc://notareal.hs/abcdef", }, "m.image": { "width": 668, }, "m.caption": [ { "body": "Look at my cat!", } ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Upload: my_cat.png")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert_matches!(content.file.encryption_info, None); assert_eq!(content.image.width, Some(uint!(668))); assert_eq!(content.image.height, None); assert_eq!(content.thumbnail.len(), 0); let caption = content.caption.unwrap(); assert_eq!(caption.find_plain(), Some("Look at my cat!")); } #[test] fn encrypted_content_deserialization() { let json_data = json!({ "org.matrix.msc1767.text": "Upload: my_cat.png", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" }, "m.image": {}, "m.thumbnail": [ { "url": "mxc://notareal.hs/thumbnail", } ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Upload: my_cat.png")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert!(content.file.encryption_info.is_some()); assert_eq!(content.image.width, None); assert_eq!(content.image.height, None); assert_eq!(content.thumbnail.len(), 1); assert_eq!(content.thumbnail[0].file.url, "mxc://notareal.hs/thumbnail"); assert_matches!(content.caption, None); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.text": "Upload: my_gnome.webp", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_gnome.webp", "mimetype": "image/webp", "size": 123_774, }, "m.image": { "width": 1300, "height": 837, } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.image", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Image(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); let content = message_event.content; assert_eq!(content.message.find_plain(), Some("Upload: my_gnome.webp")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); let info = content.file.info.unwrap(); assert_eq!(info.name.as_deref(), Some("my_gnome.webp")); assert_eq!(info.mimetype.as_deref(), Some("image/webp")); assert_eq!(info.size, Some(uint!(123_774))); assert_eq!(content.image.width, Some(uint!(1300))); assert_eq!(content.image.height, Some(uint!(837))); assert_eq!(content.thumbnail.len(), 0); } #[test] fn room_message_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::plain( "Upload: my_image.jpg".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_image.jpg", "url": "mxc://notareal.hs/file", "msgtype": "m.image", "org.matrix.msc1767.text": "Upload: my_image.jpg", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.image": {}, }) ); } #[test] fn room_message_stable_deserialization() { let json_data = json!({ "body": "Upload: my_image.jpg", "url": "mxc://notareal.hs/file", "msgtype": "m.image", "m.text": "Upload: my_image.jpg", "m.file": { "url": "mxc://notareal.hs/file", }, "m.image": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Image(content) => content); assert_eq!(content.body, "Upload: my_image.jpg"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_image.jpg"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } #[test] fn room_message_unstable_deserialization() { let json_data = json!({ "body": "Upload: my_image.jpg", "url": "mxc://notareal.hs/file", "msgtype": "m.image", "org.matrix.msc1767.text": "Upload: my_image.jpg", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.image": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Image(content) => content); assert_eq!(content.body, "Upload: my_image.jpg"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_image.jpg"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } ruma-common-0.10.5/tests/events/initial_state.rs000064400000000000000000000006471046102023000200150ustar 00000000000000use assert_matches::assert_matches; use ruma_common::events::AnyInitialStateEvent; use serde_json::json; #[test] fn deserialize_initial_state_event() { let ev = serde_json::from_value(json!({ "type": "m.room.name", "content": { "name": "foo" } })) .unwrap(); let ev = assert_matches!(ev, AnyInitialStateEvent::RoomName(ev) => ev); assert_eq!(ev.content.name.as_deref(), Some("foo")); } ruma-common-0.10.5/tests/events/location.rs000064400000000000000000000222551046102023000167730ustar 00000000000000#![cfg(feature = "unstable-msc3488")] use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ location::{AssetType, LocationContent, LocationEventContent, ZoomLevel, ZoomLevelError}, message::MessageContent, room::message::{ InReplyTo, LocationMessageEventContent, MessageType, Relation, RoomMessageEventContent, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn plain_content_serialization() { let event_content = LocationEventContent::plain( "Alice was at geo:51.5008,0.1247;u=35", LocationContent::new("geo:51.5008,0.1247;u=35".to_owned()), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35", "m.location": { "uri": "geo:51.5008,0.1247;u=35", }, }) ); } #[test] fn event_serialization() { let event = OriginalMessageLikeEvent { content: assign!( LocationEventContent::with_message( MessageContent::html( "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021", "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021", ), assign!( LocationContent::new("geo:51.5008,0.1247;u=35".to_owned()), { description: Some("Alice's whereabouts".into()), zoom_level: Some(ZoomLevel::new(4).unwrap()) } ) ), { ts: Some(MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), }), } ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.html": "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021", "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021", "m.location": { "uri": "geo:51.5008,0.1247;u=35", "description": "Alice's whereabouts", "zoom_level": 4, }, "m.ts": 1_636_829_458, "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com", }, }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.location", }) ); } #[test] fn plain_content_deserialization() { let json_data = json!({ "m.text": "Alice was at geo:51.5008,0.1247;u=35", "m.location": { "uri": "geo:51.5008,0.1247;u=35", }, }); let ev = from_json_value::(json_data).unwrap(); assert_eq!(ev.message.find_plain(), Some("Alice was at geo:51.5008,0.1247;u=35")); assert_eq!(ev.message.find_html(), None); assert_eq!(ev.location.uri, "geo:51.5008,0.1247;u=35"); assert_eq!(ev.location.description, None); assert_matches!(ev.location.zoom_level, None); assert_eq!(ev.asset.type_, AssetType::Self_); assert_eq!(ev.ts, None); } #[test] fn zoomlevel_deserialization_pass() { let json_data = json!({ "uri": "geo:51.5008,0.1247;u=35", "zoom_level": 16, }); assert_matches!( from_json_value::(json_data).unwrap(), LocationContent { zoom_level: Some(zoom_level), .. } if zoom_level.get() == uint!(16) ); } #[test] fn zoomlevel_deserialization_too_high() { let json_data = json!({ "uri": "geo:51.5008,0.1247;u=35", "zoom_level": 30, }); let err = from_json_value::(json_data).unwrap_err(); assert!(err.is_data()); assert_eq!(err.to_string(), ZoomLevelError::TooHigh.to_string()); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.message": [ { "body": "Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021" }, ], "m.location": { "uri": "geo:51.5008,0.1247;u=35", "description": "Alice's whereabouts", "zoom_level": 4, }, "m.ts": 1_636_829_458, "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com", }, }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.location", }); let ev = from_json_value::(json_data).unwrap(); let ev = assert_matches!(ev, AnyMessageLikeEvent::Location(MessageLikeEvent::Original(ev)) => ev); assert_eq!( ev.content.message.find_plain(), Some("Alice was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021") ); assert_eq!(ev.content.message.find_html(), None); assert_eq!(ev.content.location.uri, "geo:51.5008,0.1247;u=35"); assert_eq!(ev.content.location.description.as_deref(), Some("Alice's whereabouts")); assert_eq!(ev.content.location.zoom_level.unwrap().get(), uint!(4)); assert_eq!(ev.content.asset.type_, AssetType::Self_); assert_eq!(ev.content.ts, Some(MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)))); assert_eq!(ev.event_id, event_id!("$event:notareal.hs")); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(ev.room_id, room_id!("!roomid:notareal.hs")); assert_eq!(ev.sender, user_id!("@user:notareal.hs")); assert!(ev.unsigned.is_empty()); } #[test] fn room_message_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Location(LocationMessageEventContent::new( "Alice was at geo:51.5008,0.1247;u=35".to_owned(), "geo:51.5008,0.1247;u=35".to_owned(), ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Alice was at geo:51.5008,0.1247;u=35", "geo_uri": "geo:51.5008,0.1247;u=35", "msgtype": "m.location", "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35", "org.matrix.msc3488.location": { "uri": "geo:51.5008,0.1247;u=35", }, }) ); } #[test] fn room_message_stable_deserialization() { let json_data = json!({ "body": "Alice was at geo:51.5008,0.1247;u=35", "geo_uri": "geo:51.5008,0.1247;u=35", "msgtype": "m.location", "m.text": "Alice was at geo:51.5008,0.1247;u=35", "m.location": { "uri": "geo:51.5008,0.1247;u=35", }, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Location(c) => c); assert_eq!(content.body, "Alice was at geo:51.5008,0.1247;u=35"); assert_eq!(content.geo_uri, "geo:51.5008,0.1247;u=35"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Alice was at geo:51.5008,0.1247;u=35"); assert_eq!(content.location.unwrap().uri, "geo:51.5008,0.1247;u=35"); } #[test] fn room_message_unstable_deserialization() { let json_data = json!({ "body": "Alice was at geo:51.5008,0.1247;u=35", "geo_uri": "geo:51.5008,0.1247;u=35", "msgtype": "m.location", "org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35", "org.matrix.msc3488.location": { "uri": "geo:51.5008,0.1247;u=35", }, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Location(c) => c); assert_eq!(content.body, "Alice was at geo:51.5008,0.1247;u=35"); assert_eq!(content.geo_uri, "geo:51.5008,0.1247;u=35"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Alice was at geo:51.5008,0.1247;u=35"); assert_eq!(content.location.unwrap().uri, "geo:51.5008,0.1247;u=35"); } ruma-common-0.10.5/tests/events/message.rs000064400000000000000000000632221046102023000166060ustar 00000000000000#![cfg(feature = "unstable-msc1767")] use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ emote::EmoteEventContent, message::{MessageContent, MessageEventContent, Text}, notice::NoticeEventContent, room::message::{ EmoteMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn try_from_valid() { let message = MessageContent::try_from(vec![Text::plain("A message")]).unwrap(); assert_eq!(message.len(), 1); } #[test] fn try_from_invalid() { assert_matches!(MessageContent::try_from(vec![]), Err(_)); } #[test] fn html_content_serialization() { let message_event_content = MessageEventContent::html("Hello, World!", "Hello, World!"); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "org.matrix.msc1767.html": "Hello, World!", "org.matrix.msc1767.text": "Hello, World!", }) ); } #[test] fn plain_text_content_serialization() { let message_event_content = MessageEventContent::plain("> <@test:example.com> test\n\ntest reply"); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", }) ); } #[test] fn unknown_mimetype_content_serialization() { let message_event_content = MessageEventContent::from( MessageContent::try_from(vec![ Text::plain("> <@test:example.com> test\n\ntest reply"), Text::new( "application/json", r#"{ "quote": "<@test:example.com> test", "reply": "test reply" }"#, ), ]) .unwrap(), ); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "org.matrix.msc1767.message": [ { "body": "> <@test:example.com> test\n\ntest reply", "mimetype": "text/plain", }, { "body": r#"{ "quote": "<@test:example.com> test", "reply": "test reply" }"#, "mimetype": "application/json", }, ] }) ); } #[test] #[cfg(feature = "markdown")] fn markdown_content_serialization() { let formatted_message = MessageEventContent::markdown("Testing **bold** and _italic_!"); assert_eq!( to_json_value(&formatted_message).unwrap(), json!({ "org.matrix.msc1767.html": "

Testing bold and italic!

\n", "org.matrix.msc1767.text": "Testing **bold** and _italic_!", }) ); let plain_message_simple = MessageEventContent::markdown("Testing a simple phrase…"); assert_eq!( to_json_value(&plain_message_simple).unwrap(), json!({ "org.matrix.msc1767.text": "Testing a simple phrase…", }) ); let plain_message_paragraphs = MessageEventContent::markdown("Testing\n\nSeveral\n\nParagraphs."); assert_eq!( to_json_value(&plain_message_paragraphs).unwrap(), json!({ "org.matrix.msc1767.html": "

Testing

\n

Several

\n

Paragraphs.

\n", "org.matrix.msc1767.text": "Testing\n\nSeveral\n\nParagraphs.", }) ); } #[test] fn relates_to_content_serialization() { let message_event_content = assign!(MessageEventContent::plain("> <@test:example.com> test\n\ntest reply"), { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new( event_id!("$15827405538098VGFWH:example.com").to_owned(), ), }), }); let json_data = json!({ "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", "m.relates_to": { "m.in_reply_to": { "event_id": "$15827405538098VGFWH:example.com" } } }); assert_eq!(to_json_value(&message_event_content).unwrap(), json_data); } #[test] fn message_event_serialization() { let event = OriginalMessageLikeEvent { content: MessageEventContent::plain("Hello, World!"), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.text": "Hello, World!", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.message", }) ); } #[test] fn plain_text_content_unstable_deserialization() { let json_data = json!({ "org.matrix.msc1767.text": "This is my body", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("This is my body")); assert_eq!(content.message.find_html(), None); } #[test] fn plain_text_content_stable_deserialization() { let json_data = json!({ "m.text": "This is my body", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("This is my body")); assert_eq!(content.message.find_html(), None); } #[test] fn html_content_unstable_deserialization() { let json_data = json!({ "org.matrix.msc1767.html": "Hello, New World!", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), None); assert_eq!(content.message.find_html(), Some("Hello, New World!")); } #[test] fn html_content_stable_deserialization() { let json_data = json!({ "m.html": "Hello, New World!", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), None); assert_eq!(content.message.find_html(), Some("Hello, New World!")); } #[test] fn html_and_text_content_unstable_deserialization() { let json_data = json!({ "org.matrix.msc1767.html": "Hello, New World!", "org.matrix.msc1767.text": "Hello, New World!", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Hello, New World!")); assert_eq!(content.message.find_html(), Some("Hello, New World!")); } #[test] fn html_and_text_content_stable_deserialization() { let json_data = json!({ "m.html": "Hello, New World!", "m.text": "Hello, New World!", }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Hello, New World!")); assert_eq!(content.message.find_html(), Some("Hello, New World!")); } #[test] fn message_content_unstable_deserialization() { let json_data = json!({ "org.matrix.msc1767.message": [ { "body": "Hello, New World!", "mimetype": "text/html"}, { "body": "Hello, New World!" }, ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Hello, New World!")); assert_eq!(content.message.find_html(), Some("Hello, New World!")); } #[test] fn message_content_stable_deserialization() { let json_data = json!({ "m.message": [ { "body": "Hello, New World!", "mimetype": "text/html"}, { "body": "Hello, New World!" }, ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Hello, New World!")); assert_eq!(content.message.find_html(), Some("Hello, New World!")); } #[test] fn relates_to_content_deserialization() { let json_data = json!({ "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", "m.relates_to": { "m.in_reply_to": { "event_id": "$15827405538098VGFWH:example.com" } } }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("> <@test:example.com> test\n\ntest reply")); assert_eq!(content.message.find_html(), None); let event_id = assert_matches!( content.relates_to, Some(Relation::Reply { in_reply_to: InReplyTo { event_id, .. } }) => event_id ); assert_eq!(event_id, "$15827405538098VGFWH:example.com"); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.text": "Hello, World!", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.message", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Message(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.content.message.find_plain(), Some("Hello, World!")); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); } #[test] fn room_message_plain_text_stable_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.text", "m.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "test"); } #[test] fn room_message_plain_text_unstable_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.text", "org.matrix.msc1767.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "test"); } #[test] fn room_message_html_and_text_stable_deserialization() { let json_data = json!({ "body": "test", "formatted_body": "

test

", "format": "org.matrix.custom.html", "msgtype": "m.text", "m.html": "

test

", "m.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let formatted = content.formatted.unwrap(); assert_eq!(formatted.body, "

test

"); let message = content.message.unwrap(); assert_eq!(message.len(), 2); assert_eq!(message[0].body, "

test

"); assert_eq!(message[1].body, "test"); } #[test] fn room_message_html_and_text_unstable_deserialization() { let json_data = json!({ "body": "test", "formatted_body": "

test

", "format": "org.matrix.custom.html", "msgtype": "m.text", "org.matrix.msc1767.html": "

test

", "org.matrix.msc1767.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let formatted = content.formatted.unwrap(); assert_eq!(formatted.body, "

test

"); let message = content.message.unwrap(); assert_eq!(message.len(), 2); assert_eq!(message[0].body, "

test

"); assert_eq!(message[1].body, "test"); } #[test] fn room_message_message_stable_deserialization() { let json_data = json!({ "body": "test", "formatted_body": "

test

", "format": "org.matrix.custom.html", "msgtype": "m.text", "m.message": [ { "body": "

test

", "mimetype": "text/html" }, { "body": "test", "mimetype": "text/plain" }, ], }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let formatted = content.formatted.unwrap(); assert_eq!(formatted.body, "

test

"); let message = content.message.unwrap(); assert_eq!(message.len(), 2); assert_eq!(message[0].body, "

test

"); assert_eq!(message[1].body, "test"); } #[test] fn room_message_message_unstable_deserialization() { let json_data = json!({ "body": "test", "formatted_body": "

test

", "format": "org.matrix.custom.html", "msgtype": "m.text", "org.matrix.msc1767.message": [ { "body": "

test

", "mimetype": "text/html" }, { "body": "test", "mimetype": "text/plain" }, ], }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let formatted = content.formatted.unwrap(); assert_eq!(formatted.body, "

test

"); let message = content.message.unwrap(); assert_eq!(message.len(), 2); assert_eq!(message[0].body, "

test

"); assert_eq!(message[1].body, "test"); } #[test] fn notice_event_serialization() { let event = OriginalMessageLikeEvent { content: NoticeEventContent::plain("Hello, I'm a robot!"), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.text": "Hello, I'm a robot!", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.notice", }) ); } #[test] fn room_message_notice_serialization() { let message_event_content = RoomMessageEventContent::notice_plain("> <@test:example.com> test\n\ntest reply"); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "> <@test:example.com> test\n\ntest reply", "msgtype": "m.notice", "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", }) ); } #[test] fn notice_event_stable_deserialization() { let json_data = json!({ "content": { "m.message": [ { "body": "Hello, I'm a robot!", "mimetype": "text/html"}, { "body": "Hello, I'm a robot!" }, ] }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.notice", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Notice(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); let message = message_event.content.message; assert_eq!(message.find_plain(), Some("Hello, I'm a robot!")); assert_eq!(message.find_html(), Some("Hello, I'm a robot!")); } #[test] fn notice_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.message": [ { "body": "Hello, I'm a robot!", "mimetype": "text/html"}, { "body": "Hello, I'm a robot!" }, ] }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.notice", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Notice(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); let message = message_event.content.message; assert_eq!(message.find_plain(), Some("Hello, I'm a robot!")); assert_eq!(message.find_html(), Some("Hello, I'm a robot!")); } #[test] fn room_message_notice_stable_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.notice", "m.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Notice(content), .. }) => content ); assert_eq!(content.body, "test"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "test"); } #[test] fn room_message_notice_unstable_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.notice", "org.matrix.msc1767.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Notice(content), .. }) => content ); assert_eq!(content.body, "test"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "test"); } #[test] fn emote_event_serialization() { let event = OriginalMessageLikeEvent { content: EmoteEventContent::html( "is testing some code…", "is testing some code…", ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.html": "is testing some code…", "org.matrix.msc1767.text": "is testing some code…", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.emote", }) ); } #[test] fn room_message_emote_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Emote( EmoteMessageEventContent::plain("> <@test:example.com> test\n\ntest reply"), )); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "> <@test:example.com> test\n\ntest reply", "msgtype": "m.emote", "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", }) ); } #[test] fn emote_event_stable_deserialization() { let json_data = json!({ "content": { "m.text": "is testing some code…", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.emote", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Emote(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); let message = message_event.content.message; assert_eq!(message.find_plain(), Some("is testing some code…")); assert_eq!(message.find_html(), None); } #[test] fn emote_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc1767.text": "is testing some code…", }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.emote", }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Emote(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$event:notareal.hs"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(message_event.room_id, "!roomid:notareal.hs"); assert_eq!(message_event.sender, "@user:notareal.hs"); assert!(message_event.unsigned.is_empty()); let message = message_event.content.message; assert_eq!(message.find_plain(), Some("is testing some code…")); assert_eq!(message.find_html(), None); } #[test] fn room_message_emote_stable_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.emote", "m.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Emote(content), .. }) => content ); assert_eq!(content.body, "test"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "test"); } #[test] fn room_message_emote_unstable_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.emote", "org.matrix.msc1767.text": "test", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Emote(content), .. }) => content ); assert_eq!(content.body, "test"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "test"); } #[test] #[cfg(feature = "unstable-msc3554")] fn lang_serialization() { let content = MessageContent::try_from(vec![ assign!(Text::plain("Bonjour le monde !"), { lang: Some("fr".into()) }), assign!(Text::plain("Hallo Welt!"), { lang: Some("de".into()) }), assign!(Text::plain("Hello World!"), { lang: Some("en".into()) }), ]) .unwrap(); assert_eq!( to_json_value(&content).unwrap(), json!({ "org.matrix.msc1767.message": [ { "body": "Bonjour le monde !", "mimetype": "text/plain", "lang": "fr"}, { "body": "Hallo Welt!", "mimetype": "text/plain", "lang": "de"}, { "body": "Hello World!", "mimetype": "text/plain", "lang": "en"}, ] }) ); } #[test] #[cfg(feature = "unstable-msc3554")] fn lang_deserialization() { let json_data = json!({ "org.matrix.msc1767.message": [ { "body": "Bonjour le monde !", "mimetype": "text/plain", "lang": "fr"}, { "body": "Hallo Welt!", "mimetype": "text/plain", "lang": "de"}, { "body": "Hello World!", "mimetype": "text/plain", "lang": "en"}, ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content[0].lang.as_deref(), Some("fr")); assert_eq!(content[1].lang.as_deref(), Some("de")); assert_eq!(content[2].lang.as_deref(), Some("en")); } ruma-common-0.10.5/tests/events/message_event.rs000064400000000000000000000223661046102023000200130ustar 00000000000000use assert_matches::assert_matches; use assign::assign; use js_int::{uint, UInt}; use ruma_common::{ event_id, events::{ room::{ImageInfo, MediaSource, ThumbnailInfo}, sticker::StickerEventContent, AnyMessageLikeEvent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, MessageLikeEvent, MessageLikeEventType, MessageLikeUnsigned, OriginalMessageLikeEvent, }, mxc_uri, room_id, serde::{CanBeEmpty, Raw}, user_id, MilliSecondsSinceUnixEpoch, VoipVersionId, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn message_serialize_sticker() { let aliases_event = OriginalMessageLikeEvent { content: StickerEventContent::new( "Hello".into(), assign!(ImageInfo::new(), { height: UInt::new(423), width: UInt::new(1011), mimetype: Some("image/png".into()), size: UInt::new(84242), thumbnail_info: Some(Box::new(assign!(ThumbnailInfo::new(), { width: UInt::new(800), height: UInt::new(334), mimetype: Some("image/png".into()), size: UInt::new(82595), }))), thumbnail_source: Some(MediaSource::Plain(mxc_uri!("mxc://matrix.org/irsns989Rrsn").to_owned())), }), mxc_uri!("mxc://matrix.org/rnsldl8srs98IRrs").to_owned(), ), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!roomid:room.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let actual = to_json_value(&aliases_event).unwrap(); #[cfg(not(feature = "unstable-msc3552"))] let expected = json!({ "content": { "body": "Hello", "info": { "h": 423, "mimetype": "image/png", "size": 84242, "thumbnail_info": { "h": 334, "mimetype": "image/png", "size": 82595, "w": 800 }, "thumbnail_url": "mxc://matrix.org/irsns989Rrsn", "w": 1011 }, "url": "mxc://matrix.org/rnsldl8srs98IRrs" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "type": "m.sticker", }); #[cfg(feature = "unstable-msc3552")] let expected = json!({ "content": { "body": "Hello", "info": { "h": 423, "mimetype": "image/png", "size": 84242, "thumbnail_info": { "h": 334, "mimetype": "image/png", "size": 82595, "w": 800 }, "thumbnail_url": "mxc://matrix.org/irsns989Rrsn", "w": 1011 }, "url": "mxc://matrix.org/rnsldl8srs98IRrs", "org.matrix.msc1767.text": "Hello", "org.matrix.msc1767.file": { "url": "mxc://matrix.org/rnsldl8srs98IRrs", "mimetype": "image/png", "size": 84242, }, "org.matrix.msc1767.image": { "height": 423, "width": 1011, }, "org.matrix.msc1767.thumbnail": [ { "url": "mxc://matrix.org/irsns989Rrsn", "mimetype": "image/png", "size": 82595, "height": 334, "width": 800, } ], }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "type": "m.sticker", }); assert_eq!(actual, expected); } #[test] fn deserialize_message_call_answer_content() { let json_data = json!({ "answer": { "type": "answer", "sdp": "Hello" }, "call_id": "foofoo", "version": 0 }); let content = assert_matches!( from_json_value::>(json_data) .unwrap() .deserialize_content(MessageLikeEventType::CallAnswer) .unwrap(), AnyMessageLikeEventContent::CallAnswer(content) => content ); assert_eq!(content.answer.sdp, "Hello"); assert_eq!(content.call_id, "foofoo"); assert_eq!(content.version, VoipVersionId::V0); } #[test] fn deserialize_message_call_answer() { let json_data = json!({ "content": { "answer": { "type": "answer", "sdp": "Hello" }, "call_id": "foofoo", "version": 0 }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "type": "m.call.answer" }); let message_event = assert_matches!( from_json_value::(json_data).unwrap(), AnyMessageLikeEvent::CallAnswer(MessageLikeEvent::Original(message_event)) => message_event ); assert_eq!(message_event.event_id, "$h29iv0s8:example.com"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(message_event.room_id, "!roomid:room.com"); assert_eq!(message_event.sender, "@carl:example.com"); assert!(message_event.unsigned.is_empty()); let content = message_event.content; assert_eq!(content.answer.sdp, "Hello"); assert_eq!(content.call_id, "foofoo"); assert_eq!(content.version, VoipVersionId::V0); } #[test] fn deserialize_message_sticker() { let json_data = json!({ "content": { "body": "Hello", "info": { "h": 423, "mimetype": "image/png", "size": 84242, "thumbnail_info": { "h": 334, "mimetype": "image/png", "size": 82595, "w": 800 }, "thumbnail_url": "mxc://matrix.org/irnsNRS2879", "w": 1011 }, "url": "mxc://matrix.org/jxPXTKpyydzdHJkdFNZjTZrD" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "type": "m.sticker" }); let message_event = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Sticker(MessageLikeEvent::Original(message_event))) => message_event ); assert_eq!(message_event.event_id, "$h29iv0s8:example.com"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(message_event.room_id, "!roomid:room.com"); assert_eq!(message_event.sender, "@carl:example.com"); assert!(message_event.unsigned.is_empty()); let content = message_event.content; assert_eq!(content.body, "Hello"); assert_eq!(content.info.height, Some(uint!(423))); assert_eq!(content.info.width, Some(uint!(1011))); assert_eq!(content.info.mimetype.as_deref(), Some("image/png")); assert_eq!(content.info.size, Some(uint!(84242))); assert_eq!(content.url, "mxc://matrix.org/jxPXTKpyydzdHJkdFNZjTZrD"); let thumbnail_url = assert_matches!( content.info.thumbnail_source, Some(MediaSource::Plain(thumbnail_url)) => thumbnail_url ); assert_eq!(thumbnail_url, "mxc://matrix.org/irnsNRS2879"); let thumbnail_info = content.info.thumbnail_info.unwrap(); assert_eq!(thumbnail_info.width, Some(uint!(800))); assert_eq!(thumbnail_info.height, Some(uint!(334))); assert_eq!(thumbnail_info.mimetype.as_deref(), Some("image/png")); assert_eq!(thumbnail_info.size, Some(uint!(82595))); } #[test] fn deserialize_message_then_convert_to_full() { let rid = room_id!("!roomid:room.com"); let json_data = json!({ "content": { "answer": { "type": "answer", "sdp": "Hello" }, "call_id": "foofoo", "version": 0 }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "type": "m.call.answer" }); let sync_ev: AnySyncMessageLikeEvent = from_json_value(json_data).unwrap(); let message_event = assert_matches!( sync_ev.into_full_event(rid.to_owned()), AnyMessageLikeEvent::CallAnswer(MessageLikeEvent::Original(message_event)) => message_event ); assert_eq!(message_event.event_id, "$h29iv0s8:example.com"); assert_eq!(message_event.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(message_event.room_id, "!roomid:room.com"); assert_eq!(message_event.sender, "@carl:example.com"); assert!(message_event.unsigned.is_empty()); let content = message_event.content; assert_eq!(content.answer.sdp, "Hello"); assert_eq!(content.call_id, "foofoo"); assert_eq!(content.version, VoipVersionId::V0); } ruma-common-0.10.5/tests/events/mod.rs000064400000000000000000000005711046102023000157370ustar 00000000000000#![cfg(feature = "events")] mod audio; mod call; mod enums; mod ephemeral_event; mod event; mod event_content; mod event_enums; mod file; mod image; mod initial_state; mod location; mod message; mod message_event; mod pdu; mod poll; mod redacted; mod redaction; mod relations; mod room_message; mod state_event; mod sticker; mod stripped; mod to_device; mod video; mod voice; ruma-common-0.10.5/tests/events/pdu.rs000064400000000000000000000175001046102023000157500ustar 00000000000000#![cfg(all(feature = "unstable-pdu"))] use std::collections::BTreeMap; use js_int::uint; use ruma_common::{ event_id, events::{ pdu::{EventHash, Pdu, RoomV1Pdu, RoomV3Pdu}, RoomEventType, }, room_id, server_name, server_signing_key_id, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, value::to_raw_value as to_raw_json_value, }; #[test] fn serialize_pdu_as_v1() { let mut signatures = BTreeMap::new(); let mut inner_signature = BTreeMap::new(); inner_signature.insert( server_signing_key_id!("ed25519:key_version").to_owned(), "86BytesOfSignatureOfTheRedactedEvent".into(), ); signatures.insert(server_name!("example.com").to_owned(), inner_signature); let mut unsigned = BTreeMap::new(); unsigned.insert("somekey".into(), to_raw_json_value(&json!({ "a": 456 })).unwrap()); let v1_pdu = RoomV1Pdu { room_id: room_id!("!n8f893n9:example.com").to_owned(), event_id: event_id!("$somejoinevent:matrix.org").to_owned(), sender: user_id!("@sender:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(1_592_050_773_658_u64.try_into().unwrap()), kind: RoomEventType::RoomPowerLevels, content: to_raw_json_value(&json!({ "testing": 123 })).unwrap(), state_key: Some("state".into()), prev_events: vec![( event_id!("$previousevent:matrix.org").to_owned(), EventHash::new("123567".into()), )], depth: uint!(2), auth_events: vec![( event_id!("$someauthevent:matrix.org").to_owned(), EventHash::new("21389CFEDABC".into()), )], redacts: Some(event_id!("$9654:matrix.org").to_owned()), unsigned, hashes: EventHash::new("1233543bABACDEF".into()), signatures, }; let pdu = Pdu::RoomV1Pdu(v1_pdu); let json = json!({ "room_id": "!n8f893n9:example.com", "event_id": "$somejoinevent:matrix.org", "sender": "@sender:example.com", "origin_server_ts": 1_592_050_773_658_u64, "type": "m.room.power_levels", "content": { "testing": 123 }, "state_key": "state", "prev_events": [ [ "$previousevent:matrix.org", {"sha256": "123567"} ] ], "depth": 2, "auth_events": [ ["$someauthevent:matrix.org", {"sha256": "21389CFEDABC"}] ], "redacts": "$9654:matrix.org", "unsigned": { "somekey": { "a": 456 } }, "hashes": { "sha256": "1233543bABACDEF" }, "signatures": { "example.com": { "ed25519:key_version":"86BytesOfSignatureOfTheRedactedEvent" } } }); assert_eq!(to_json_value(&pdu).unwrap(), json); } #[test] fn serialize_pdu_as_v3() { let mut signatures = BTreeMap::new(); let mut inner_signature = BTreeMap::new(); inner_signature.insert( server_signing_key_id!("ed25519:key_version").to_owned(), "86BytesOfSignatureOfTheRedactedEvent".into(), ); signatures.insert(server_name!("example.com").to_owned(), inner_signature); let mut unsigned = BTreeMap::new(); unsigned.insert("somekey".into(), to_raw_json_value(&json!({ "a": 456 })).unwrap()); let v3_pdu = RoomV3Pdu { room_id: room_id!("!n8f893n9:example.com").to_owned(), sender: user_id!("@sender:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(1_592_050_773_658_u64.try_into().unwrap()), kind: RoomEventType::RoomPowerLevels, content: to_raw_json_value(&json!({ "testing": 123 })).unwrap(), state_key: Some("state".into()), prev_events: vec![event_id!("$previousevent:matrix.org").to_owned()], depth: uint!(2), auth_events: vec![event_id!("$someauthevent:matrix.org").to_owned()], redacts: Some(event_id!("$9654:matrix.org").to_owned()), unsigned, hashes: EventHash::new("1233543bABACDEF".into()), signatures, }; let pdu_stub = Pdu::RoomV3Pdu(v3_pdu); let json = json!({ "room_id": "!n8f893n9:example.com", "sender": "@sender:example.com", "origin_server_ts": 1_592_050_773_658_u64, "type": "m.room.power_levels", "content": { "testing": 123 }, "state_key": "state", "prev_events": [ "$previousevent:matrix.org" ], "depth": 2, "auth_events": ["$someauthevent:matrix.org" ], "redacts": "$9654:matrix.org", "unsigned": { "somekey": { "a": 456 } }, "hashes": { "sha256": "1233543bABACDEF" }, "signatures": { "example.com": { "ed25519:key_version":"86BytesOfSignatureOfTheRedactedEvent" } } }); assert_eq!(to_json_value(&pdu_stub).unwrap(), json); } #[test] fn deserialize_pdu_as_v1() { let json = json!({ "room_id": "!n8f893n9:example.com", "event_id": "$somejoinevent:matrix.org", "auth_events": [ [ "$abc123:matrix.org", { "sha256": "Base64EncodedSha256HashesShouldBe43BytesLong" } ] ], "content": { "key": "value" }, "depth": 12, "event_id": "$a4ecee13e2accdadf56c1025:example.com", "hashes": { "sha256": "ThisHashCoversAllFieldsInCaseThisIsRedacted" }, "origin_server_ts": 1_234_567_890, "prev_events": [ [ "$abc123:matrix.org", { "sha256": "Base64EncodedSha256HashesShouldBe43BytesLong" } ] ], "redacts": "$def456:matrix.org", "room_id": "!abc123:matrix.org", "sender": "@someone:matrix.org", "signatures": { "example.com": { "ed25519:key_version": "86BytesOfSignatureOfTheRedactedEvent" } }, "state_key": "my_key", "type": "m.room.message", "unsigned": { "key": "value" } }); let parsed = from_json_value::(json).unwrap(); match parsed { Pdu::RoomV1Pdu(v1_pdu) => { assert_eq!(v1_pdu.auth_events.first().unwrap().0, event_id!("$abc123:matrix.org")); assert_eq!( v1_pdu.auth_events.first().unwrap().1.sha256, "Base64EncodedSha256HashesShouldBe43BytesLong" ); } Pdu::RoomV3Pdu(_) => panic!("Matched V3 PDU"), #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } #[test] fn deserialize_pdu_as_v3() { let json = json!({ "room_id": "!n8f893n9:example.com", "auth_events": [ "$abc123:matrix.org" ], "content": { "key": "value" }, "depth": 12, "hashes": { "sha256": "ThisHashCoversAllFieldsInCaseThisIsRedacted" }, "origin_server_ts": 1_234_567_890, "prev_events": [ "$abc123:matrix.org" ], "redacts": "$def456:matrix.org", "room_id": "!abc123:matrix.org", "sender": "@someone:matrix.org", "signatures": { "example.com": { "ed25519:key_version": "86BytesOfSignatureOfTheRedactedEvent" } }, "state_key": "my_key", "type": "m.room.message", "unsigned": { "key": "value" } }); let parsed = from_json_value::(json).unwrap(); match parsed { Pdu::RoomV1Pdu(_) => panic!("Matched V1 PDU"), Pdu::RoomV3Pdu(v3_pdu) => { assert_eq!(v3_pdu.auth_events.first().unwrap(), event_id!("$abc123:matrix.org")); } #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } ruma-common-0.10.5/tests/events/poll.rs000064400000000000000000000405041046102023000161260ustar 00000000000000#![cfg(feature = "unstable-msc3381")] use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ message::MessageContent, poll::{ end::{PollEndContent, PollEndEventContent}, response::{PollResponseContent, PollResponseEventContent}, start::{ PollAnswer, PollAnswers, PollAnswersError, PollKind, PollStartContent, PollStartEventContent, }, ReferenceRelation, }, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, room_id, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn poll_answers_deserialization_valid() { let json_data = json!([ { "id": "aaa", "m.text": "First answer" }, { "id": "bbb", "m.text": "Second answer" }, ]); let answers = from_json_value::(json_data).unwrap(); assert_eq!(answers.answers().len(), 2); } #[test] fn poll_answers_deserialization_truncate() { let json_data = json!([ { "id": "aaa", "m.text": "1st answer" }, { "id": "bbb", "m.text": "2nd answer" }, { "id": "ccc", "m.text": "3rd answer" }, { "id": "ddd", "m.text": "4th answer" }, { "id": "eee", "m.text": "5th answer" }, { "id": "fff", "m.text": "6th answer" }, { "id": "ggg", "m.text": "7th answer" }, { "id": "hhh", "m.text": "8th answer" }, { "id": "iii", "m.text": "9th answer" }, { "id": "jjj", "m.text": "10th answer" }, { "id": "kkk", "m.text": "11th answer" }, { "id": "lll", "m.text": "12th answer" }, { "id": "mmm", "m.text": "13th answer" }, { "id": "nnn", "m.text": "14th answer" }, { "id": "ooo", "m.text": "15th answer" }, { "id": "ppp", "m.text": "16th answer" }, { "id": "qqq", "m.text": "17th answer" }, { "id": "rrr", "m.text": "18th answer" }, { "id": "sss", "m.text": "19th answer" }, { "id": "ttt", "m.text": "20th answer" }, { "id": "uuu", "m.text": "21th answer" }, { "id": "vvv", "m.text": "22th answer" }, ]); let answers = from_json_value::(json_data).unwrap(); assert_eq!(answers.answers().len(), 20); } #[test] fn poll_answers_deserialization_not_enough() { let json_data = json!([]); let err = from_json_value::(json_data).unwrap_err(); assert!(err.is_data()); assert_eq!(err.to_string(), PollAnswersError::NotEnoughValues.to_string()); } #[test] fn start_content_serialization() { let event_content = PollStartEventContent::new(PollStartContent::new( MessageContent::plain("How's the weather?"), PollKind::Undisclosed, vec![ PollAnswer::new("not-bad".to_owned(), MessageContent::plain("Not bad…")), PollAnswer::new("fine".to_owned(), MessageContent::plain("Fine.")), PollAnswer::new("amazing".to_owned(), MessageContent::plain("Amazing!")), ] .try_into() .unwrap(), )); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc3381.poll.start": { "question": { "org.matrix.msc1767.text": "How's the weather?" }, "kind": "org.matrix.msc3381.poll.undisclosed", "answers": [ { "id": "not-bad", "org.matrix.msc1767.text": "Not bad…"}, { "id": "fine", "org.matrix.msc1767.text": "Fine."}, { "id": "amazing", "org.matrix.msc1767.text": "Amazing!"}, ], }, }) ); } #[test] fn start_event_serialization() { let event = OriginalMessageLikeEvent { content: PollStartEventContent::new(assign!( PollStartContent::new( MessageContent::plain("How's the weather?"), PollKind::Disclosed, vec![ PollAnswer::new("not-bad".to_owned(), MessageContent::plain("Not bad…")), PollAnswer::new("fine".to_owned(), MessageContent::plain("Fine.")), PollAnswer::new("amazing".to_owned(), MessageContent::plain("Amazing!")), ] .try_into() .unwrap(), ), { max_selections: uint!(2) } )), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc3381.poll.start": { "question": { "org.matrix.msc1767.text": "How's the weather?" }, "kind": "org.matrix.msc3381.poll.disclosed", "max_selections": 2, "answers": [ { "id": "not-bad", "org.matrix.msc1767.text": "Not bad…"}, { "id": "fine", "org.matrix.msc1767.text": "Fine."}, { "id": "amazing", "org.matrix.msc1767.text": "Amazing!"}, ] }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "org.matrix.msc3381.poll.start", }) ); } #[test] fn start_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc3381.poll.start": { "question": { "org.matrix.msc1767.text": "How's the weather?" }, "kind": "org.matrix.msc3381.poll.undisclosed", "max_selections": 2, "answers": [ { "id": "not-bad", "org.matrix.msc1767.text": "Not bad…"}, { "id": "fine", "org.matrix.msc1767.text": "Fine."}, { "id": "amazing", "org.matrix.msc1767.text": "Amazing!"}, ] }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "org.matrix.msc3381.poll.start", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::PollStart(MessageLikeEvent::Original(message_event)) => message_event ); let poll_start = message_event.content.poll_start; assert_eq!(poll_start.question[0].body, "How's the weather?"); assert_eq!(poll_start.kind, PollKind::Undisclosed); assert_eq!(poll_start.max_selections, uint!(2)); let answers = poll_start.answers.answers(); assert_eq!(answers.len(), 3); assert_eq!(answers[0].id, "not-bad"); assert_eq!(answers[0].answer[0].body, "Not bad…"); assert_eq!(answers[1].id, "fine"); assert_eq!(answers[1].answer[0].body, "Fine."); assert_eq!(answers[2].id, "amazing"); assert_eq!(answers[2].answer[0].body, "Amazing!"); } #[test] fn start_event_stable_deserialization() { let json_data = json!({ "content": { "m.poll.start": { "question": { "m.text": "How's the weather?" }, "kind": "m.poll.disclosed", "answers": [ { "id": "not-bad", "m.text": "Not bad…"}, { "id": "fine", "m.text": "Fine."}, { "id": "amazing", "m.text": "Amazing!"}, ] }, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.poll.start", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::PollStart(MessageLikeEvent::Original(message_event)) => message_event ); let poll_start = message_event.content.poll_start; assert_eq!(poll_start.question[0].body, "How's the weather?"); assert_eq!(poll_start.kind, PollKind::Disclosed); assert_eq!(poll_start.max_selections, uint!(1)); let answers = poll_start.answers.answers(); assert_eq!(answers.len(), 3); assert_eq!(answers[0].id, "not-bad"); assert_eq!(answers[0].answer[0].body, "Not bad…"); assert_eq!(answers[1].id, "fine"); assert_eq!(answers[1].answer[0].body, "Fine."); assert_eq!(answers[2].id, "amazing"); assert_eq!(answers[2].answer[0].body, "Amazing!"); } #[test] fn response_content_serialization() { let event_content = PollResponseEventContent::new( PollResponseContent::new(vec!["my-answer".to_owned()]), event_id!("$related_event:notareal.hs").to_owned(), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc3381.poll.response": { "answers": ["my-answer"], }, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }) ); } #[test] fn response_event_serialization() { let event = OriginalMessageLikeEvent { content: PollResponseEventContent::new( PollResponseContent::new(vec!["first-answer".to_owned(), "second-answer".to_owned()]), event_id!("$related_event:notareal.hs").to_owned(), ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc3381.poll.response": { "answers": ["first-answer", "second-answer"], }, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "org.matrix.msc3381.poll.response", }) ); } #[test] fn response_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc3381.poll.response": { "answers": ["my-answer"], }, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "org.matrix.msc3381.poll.response", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::PollResponse(MessageLikeEvent::Original(message_event)) => message_event ); let answers = message_event.content.poll_response.answers; assert_eq!(answers.len(), 1); assert_eq!(answers[0], "my-answer"); let event_id = assert_matches!( message_event.content.relates_to, ReferenceRelation { event_id, .. } => event_id ); assert_eq!(event_id, "$related_event:notareal.hs"); } #[test] fn response_event_stable_deserialization() { let json_data = json!({ "content": { "m.poll.response": { "answers": ["first-answer", "second-answer"], }, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.poll.response", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::PollResponse(MessageLikeEvent::Original(message_event)) => message_event ); let answers = message_event.content.poll_response.answers; assert_eq!(answers.len(), 2); assert_eq!(answers[0], "first-answer"); assert_eq!(answers[1], "second-answer"); let event_id = assert_matches!( message_event.content.relates_to, ReferenceRelation { event_id, .. } => event_id ); assert_eq!(event_id, "$related_event:notareal.hs"); } #[test] fn end_content_serialization() { let event_content = PollEndEventContent::new( PollEndContent::new(), event_id!("$related_event:notareal.hs").to_owned(), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc3381.poll.end": {}, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }) ); } #[test] fn end_event_serialization() { let event = OriginalMessageLikeEvent { content: PollEndEventContent::new( PollEndContent::new(), event_id!("$related_event:notareal.hs").to_owned(), ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc3381.poll.end": {}, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "org.matrix.msc3381.poll.end", }) ); } #[test] fn end_event_unstable_deserialization() { let json_data = json!({ "content": { "org.matrix.msc3381.poll.end": {}, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "org.matrix.msc3381.poll.end", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::PollEnd(MessageLikeEvent::Original(message_event)) => message_event ); let event_id = assert_matches!( message_event.content.relates_to, ReferenceRelation { event_id, .. } => event_id ); assert_eq!(event_id, "$related_event:notareal.hs"); } #[test] fn end_event_stable_deserialization() { let json_data = json!({ "content": { "m.poll.end": {}, "m.relates_to": { "rel_type": "m.reference", "event_id": "$related_event:notareal.hs", } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.poll.end", }); let event = from_json_value::(json_data).unwrap(); let message_event = assert_matches!( event, AnyMessageLikeEvent::PollEnd(MessageLikeEvent::Original(message_event)) => message_event ); let event_id = assert_matches!( message_event.content.relates_to, ReferenceRelation { event_id, .. } => event_id ); assert_eq!(event_id, "$related_event:notareal.hs"); } ruma-common-0.10.5/tests/events/redacted.rs000064400000000000000000000245631046102023000167420ustar 00000000000000use assert_matches::assert_matches; use js_int::uint; use ruma_common::{ event_id, events::{ room::{ aliases::RedactedRoomAliasesEventContent, create::{RedactedRoomCreateEventContent, RoomCreateEventContent}, message::{RedactedRoomMessageEventContent, RoomMessageEventContent}, redaction::{ OriginalSyncRoomRedactionEvent, RoomRedactionEventContent, SyncRoomRedactionEvent, }, }, AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, EventContent, MessageLikeEvent, MessageLikeUnsigned, RedactContent, RedactedSyncMessageLikeEvent, RedactedSyncStateEvent, RedactedUnsigned, SyncMessageLikeEvent, SyncStateEvent, }, server_name, user_id, MilliSecondsSinceUnixEpoch, RoomVersionId, }; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, value::to_raw_value as to_raw_json_value, }; fn unsigned() -> RedactedUnsigned { let mut unsigned = RedactedUnsigned::default(); unsigned.redacted_because = Some(Box::new(SyncRoomRedactionEvent::Original(OriginalSyncRoomRedactionEvent { content: RoomRedactionEventContent::with_reason("redacted because".into()), redacts: event_id!("$h29iv0s8:example.com").to_owned(), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), sender: user_id!("@carl:example.com").to_owned(), unsigned: MessageLikeUnsigned::default(), }))); unsigned } #[test] fn redacted_message_event_serialize() { let redacted = RedactedSyncMessageLikeEvent { content: RedactedRoomMessageEventContent::new(), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), sender: user_id!("@carl:example.com").to_owned(), unsigned: RedactedUnsigned::default(), }; let expected = json!({ "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "type": "m.room.message", }); let actual = to_json_value(&redacted).unwrap(); assert_eq!(actual, expected); } #[test] fn redacted_aliases_event_serialize_no_content() { let redacted = RedactedSyncStateEvent { content: RedactedRoomAliasesEventContent::default(), event_id: event_id!("$h29iv0s8:example.com").to_owned(), state_key: server_name!("example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), sender: user_id!("@carl:example.com").to_owned(), unsigned: RedactedUnsigned::default(), }; let expected = json!({ "event_id": "$h29iv0s8:example.com", "state_key": "example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "type": "m.room.aliases", }); let actual = to_json_value(&redacted).unwrap(); assert_eq!(actual, expected); } #[test] fn redacted_aliases_event_serialize_with_content() { let redacted = RedactedSyncStateEvent { content: RedactedRoomAliasesEventContent::new_v1(vec![]), event_id: event_id!("$h29iv0s8:example.com").to_owned(), state_key: server_name!("example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), sender: user_id!("@carl:example.com").to_owned(), unsigned: RedactedUnsigned::default(), }; let expected = json!({ "content": { "aliases": [] }, "event_id": "$h29iv0s8:example.com", "state_key": "example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "type": "m.room.aliases", }); let actual = to_json_value(&redacted).unwrap(); assert_eq!(actual, expected); } #[test] fn redacted_aliases_deserialize() { let redacted = json!({ "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "state_key": "hello", "unsigned": unsigned(), "type": "m.room.aliases", }); let actual = to_json_value(&redacted).unwrap(); let redacted = assert_matches!( from_json_value::(actual), Ok(AnySyncTimelineEvent::State(AnySyncStateEvent::RoomAliases( SyncStateEvent::Redacted(redacted), ))) => redacted ); assert_eq!(redacted.event_id, "$h29iv0s8:example.com"); assert_eq!(redacted.content.aliases, None); } #[test] fn redacted_deserialize_any_room() { let redacted = json!({ "event_id": "$h29iv0s8:example.com", "room_id": "!roomid:room.com", "origin_server_ts": 1, "sender": "@carl:example.com", "unsigned": unsigned(), "type": "m.room.message", }); let actual = to_json_value(&redacted).unwrap(); let redacted = assert_matches!( from_json_value::(actual), Ok(AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage( MessageLikeEvent::Redacted(redacted), ))) => redacted ); assert_eq!(redacted.event_id, "$h29iv0s8:example.com"); assert_eq!(redacted.room_id, "!roomid:room.com"); } #[test] fn redacted_deserialize_any_room_sync() { let mut unsigned = RedactedUnsigned::default(); // The presence of `redacted_because` triggers the event enum (AnySyncTimelineEvent in this // case) to return early with `RedactedContent` instead of failing to deserialize according // to the event type string. unsigned.redacted_because = Some(Box::new(SyncRoomRedactionEvent::Original(OriginalSyncRoomRedactionEvent { content: RoomRedactionEventContent::with_reason("redacted because".into()), redacts: event_id!("$h29iv0s8:example.com").to_owned(), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), sender: user_id!("@carl:example.com").to_owned(), unsigned: MessageLikeUnsigned::default(), }))); let redacted = json!({ "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "unsigned": unsigned, "type": "m.room.message", }); let actual = to_json_value(&redacted).unwrap(); let redacted = assert_matches!( from_json_value::(actual), Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( SyncMessageLikeEvent::Redacted(redacted), ))) => redacted ); assert_eq!(redacted.event_id, "$h29iv0s8:example.com"); } #[test] fn redacted_state_event_deserialize() { let redacted = json!({ "content": { "creator": "@carl:example.com", }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "state_key": "", "unsigned": unsigned(), "type": "m.room.create", }); let redacted = assert_matches!( from_json_value::(redacted), Ok(AnySyncTimelineEvent::State(AnySyncStateEvent::RoomCreate( SyncStateEvent::Redacted(redacted), ))) => redacted ); assert_eq!(redacted.event_id, "$h29iv0s8:example.com"); assert!(redacted.unsigned.redacted_because.is_some()); assert_eq!(redacted.content.creator, "@carl:example.com"); } #[test] fn redacted_custom_event_deserialize() { let redacted = json!({ "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "sender": "@carl:example.com", "state_key": "hello there", "unsigned": unsigned(), "type": "m.made.up", }); let state_ev = assert_matches!( from_json_value::(redacted), Ok(AnySyncTimelineEvent::State(state_ev)) => state_ev ); assert_eq!(state_ev.event_id(), "$h29iv0s8:example.com"); } /* #[test] fn redact_method_properly_redacts() { let ev = json!({ "type": "m.room.message", "event_id": "$143273582443PhrSn:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@user:example.com", "content": { "body": "test", "msgtype": "m.audio", "url": "mxc://example.com/AuDi0", }, }); let redaction = OriginalSyncRoomRedactionEvent { content: RoomRedactionEventContent::with_reason("redacted because".into()), redacts: event_id!("$143273582443PhrSn:example.com").to_owned(), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), sender: user_id!("@carl:example.com").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let event: AnyMessageLikeEvent = from_json_value(ev).unwrap(); assert_matches!( event.redact(redaction, &RoomVersionId::V6), AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Redacted(RedactedMessageLikeEvent { content: RedactedRoomMessageEventContent { .. }, event_id, room_id, sender, origin_server_ts, unsigned, })) if event_id == event_id!("$143273582443PhrSn:example.com") && unsigned.redacted_because.is_some() && room_id == room_id!("!roomid:room.com") && sender == user_id!("@user:example.com") && origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(1)) ); } */ #[test] fn redact_message_content() { let json = json!({ "body": "test", "msgtype": "m.audio", "url": "mxc://example.com/AuDi0", }); let raw_json = to_raw_json_value(&json).unwrap(); let content = RoomMessageEventContent::from_parts("m.room.message", &raw_json).unwrap(); assert_matches!(content.redact(&RoomVersionId::V6), RedactedRoomMessageEventContent { .. }); } #[test] fn redact_state_content() { let json = json!({ "creator": "@carl:example.com", "m.federate": true, "room_version": "4", }); let raw_json = to_raw_json_value(&json).unwrap(); let content = RoomCreateEventContent::from_parts("m.room.create", &raw_json).unwrap(); let creator = assert_matches!( content.redact(&RoomVersionId::V6), RedactedRoomCreateEventContent { creator, .. } => creator ); assert_eq!(creator, "@carl:example.com"); } ruma-common-0.10.5/tests/events/redaction.rs000064400000000000000000000041121046102023000171230ustar 00000000000000use assert_matches::assert_matches; use js_int::uint; use ruma_common::{ event_id, events::{ room::redaction::{ OriginalRoomRedactionEvent, RoomRedactionEvent, RoomRedactionEventContent, }, AnyMessageLikeEvent, MessageLikeUnsigned, }, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, }; fn redaction() -> JsonValue { json!({ "content": { "reason": "being a turd" }, "redacts": "$nomore:example.com", "event_id": "$h29iv0s8:example.com", "sender": "@carl:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "type": "m.room.redaction" }) } #[test] fn serialize_redaction() { let aliases_event = OriginalRoomRedactionEvent { content: RoomRedactionEventContent::with_reason("being a turd".into()), redacts: event_id!("$nomore:example.com").to_owned(), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!roomid:room.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let actual = to_json_value(&aliases_event).unwrap(); let expected = redaction(); assert_eq!(actual, expected); } #[test] fn deserialize_redaction() { let json_data = redaction(); let ev = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::RoomRedaction(RoomRedactionEvent::Original(ev))) => ev ); assert_eq!(ev.content.reason.as_deref(), Some("being a turd")); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.redacts, "$nomore:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.room_id, "!roomid:room.com"); assert_eq!(ev.sender, "@carl:example.com"); assert!(ev.unsigned.is_empty()); } ruma-common-0.10.5/tests/events/relations.rs000064400000000000000000000215401046102023000171570ustar 00000000000000use assert_matches::assert_matches; use assign::assign; use ruma_common::{ event_id, events::room::message::{InReplyTo, MessageType, Relation, RoomMessageEventContent}, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn reply_deserialize() { let json = json!({ "msgtype": "m.text", "body": "", "m.relates_to": { "m.in_reply_to": { "event_id": "$1598361704261elfgc:localhost", }, }, }); let event_id = assert_matches!( from_json_value::(json), Ok(RoomMessageEventContent { msgtype: MessageType::Text(_), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id, .. }, .. }), .. }) => event_id ); assert_eq!(event_id, "$1598361704261elfgc:localhost"); } #[test] fn reply_serialize() { let content = assign!(RoomMessageEventContent::text_plain("This is a reply"), { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$1598361704261elfgc").to_owned()) }), }); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "This is a reply", "m.relates_to": { "m.in_reply_to": { "event_id": "$1598361704261elfgc", }, }, }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "This is a reply", "org.matrix.msc1767.text": "This is a reply", "m.relates_to": { "m.in_reply_to": { "event_id": "$1598361704261elfgc", }, }, }) ); } #[test] #[cfg(feature = "unstable-msc2676")] fn replacement_serialize() { use ruma_common::events::room::message::Replacement; let content = assign!( RoomMessageEventContent::text_plain(""), { relates_to: Some(Relation::Replacement( Replacement::new( event_id!("$1598361704261elfgc").to_owned(), Box::new(RoomMessageEventContent::text_plain("This is the new content.")), ) )) } ); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "", "m.new_content": { "body": "This is the new content.", "msgtype": "m.text", }, "m.relates_to": { "rel_type": "m.replace", "event_id": "$1598361704261elfgc", }, }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "", "org.matrix.msc1767.text": "", "m.new_content": { "body": "This is the new content.", "msgtype": "m.text", "org.matrix.msc1767.text": "This is the new content.", }, "m.relates_to": { "rel_type": "m.replace", "event_id": "$1598361704261elfgc", }, }) ); } #[test] #[cfg(feature = "unstable-msc2676")] fn replacement_deserialize() { let json = json!({ "msgtype": "m.text", "body": "", "m.new_content": { "body": "Hello! My name is bar", "msgtype": "m.text", }, "m.relates_to": { "rel_type": "m.replace", "event_id": "$1598361704261elfgc", }, }); let replacement = assert_matches!( from_json_value::(json), Ok(RoomMessageEventContent { msgtype: MessageType::Text(_), relates_to: Some(Relation::Replacement(replacement)), .. }) => replacement ); assert_eq!(replacement.event_id, "$1598361704261elfgc"); let text = assert_matches!(replacement.new_content.msgtype, MessageType::Text(text) => text); assert_eq!(text.body, "Hello! My name is bar"); } #[test] #[cfg(feature = "unstable-msc3440")] fn thread_plain_serialize() { use ruma_common::events::room::message::Thread; let content = assign!( RoomMessageEventContent::text_plain(""), { relates_to: Some(Relation::Thread( Thread::plain( event_id!("$1598361704261elfgc").to_owned(), event_id!("$latesteventid").to_owned(), ), )), } ); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "", "m.relates_to": { "rel_type": "io.element.thread", "event_id": "$1598361704261elfgc", "m.in_reply_to": { "event_id": "$latesteventid", }, "io.element.show_reply": true, }, }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "", "org.matrix.msc1767.text": "", "m.relates_to": { "rel_type": "io.element.thread", "event_id": "$1598361704261elfgc", "m.in_reply_to": { "event_id": "$latesteventid", }, "io.element.show_reply": true, }, }) ); } #[test] #[cfg(feature = "unstable-msc3440")] fn thread_reply_serialize() { use ruma_common::events::room::message::Thread; let content = assign!( RoomMessageEventContent::text_plain(""), { relates_to: Some(Relation::Thread( Thread::reply( event_id!("$1598361704261elfgc").to_owned(), event_id!("$repliedtoeventid").to_owned(), ), )), } ); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "", "m.relates_to": { "rel_type": "io.element.thread", "event_id": "$1598361704261elfgc", "m.in_reply_to": { "event_id": "$repliedtoeventid", }, }, }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(content).unwrap(), json!({ "msgtype": "m.text", "body": "", "org.matrix.msc1767.text": "", "m.relates_to": { "rel_type": "io.element.thread", "event_id": "$1598361704261elfgc", "m.in_reply_to": { "event_id": "$repliedtoeventid", }, }, }) ); } #[test] #[cfg(feature = "unstable-msc3440")] fn thread_stable_deserialize() { let json = json!({ "msgtype": "m.text", "body": "", "m.relates_to": { "rel_type": "m.thread", "event_id": "$1598361704261elfgc", "m.in_reply_to": { "event_id": "$latesteventid", }, }, }); let thread = assert_matches!( from_json_value::(json), Ok(RoomMessageEventContent { msgtype: MessageType::Text(_), relates_to: Some(Relation::Thread(thread)), .. }) => thread ); assert_eq!(thread.event_id, "$1598361704261elfgc"); assert_eq!(thread.in_reply_to.event_id, "$latesteventid"); assert!(!thread.is_falling_back); } #[test] #[cfg(feature = "unstable-msc3440")] fn thread_unstable_deserialize() { let json = json!({ "msgtype": "m.text", "body": "", "m.relates_to": { "rel_type": "io.element.thread", "event_id": "$1598361704261elfgc", "m.in_reply_to": { "event_id": "$latesteventid", }, }, }); let thread = assert_matches!( from_json_value::(json), Ok(RoomMessageEventContent { msgtype: MessageType::Text(_), relates_to: Some(Relation::Thread(thread)), .. }) => thread ); assert_eq!(thread.event_id, "$1598361704261elfgc"); assert_eq!(thread.in_reply_to.event_id, "$latesteventid"); assert!(!thread.is_falling_back); } ruma-common-0.10.5/tests/events/room_message.rs000064400000000000000000000447321046102023000176470ustar 00000000000000use std::borrow::Cow; use assert_matches::assert_matches; #[cfg(not(feature = "unstable-msc1767"))] use assign::assign; use js_int::uint; #[cfg(not(feature = "unstable-msc1767"))] use ruma_common::events::room::message::InReplyTo; #[cfg(any(feature = "unstable-msc2676", not(feature = "unstable-msc1767")))] use ruma_common::events::room::message::Relation; use ruma_common::{ event_id, events::{ key::verification::VerificationMethod, room::{ message::{ AudioMessageEventContent, KeyVerificationRequestEventContent, MessageType, OriginalRoomMessageEvent, RoomMessageEventContent, }, MediaSource, }, MessageLikeUnsigned, }, mxc_uri, room_id, user_id, MilliSecondsSinceUnixEpoch, OwnedDeviceId, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; macro_rules! json_object { ( $($key:expr => $value:expr),* $(,)? ) => { { let mut _map = serde_json::Map::::new(); $( let _ = _map.insert($key, $value); )* _map } }; } #[test] fn serialization() { let ev = OriginalRoomMessageEvent { content: RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain( "test".into(), mxc_uri!("mxc://example.org/ffed755USFFxlgbQYZGtryd").to_owned(), None, ))), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; #[cfg(not(feature = "unstable-msc3246"))] assert_eq!( to_json_value(ev).unwrap(), json!({ "type": "m.room.message", "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 10_000, "room_id": "!testroomid:example.org", "sender": "@user:example.org", "content": { "body": "test", "msgtype": "m.audio", "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd", } }) ); #[cfg(feature = "unstable-msc3246")] assert_eq!( to_json_value(ev).unwrap(), json!({ "type": "m.room.message", "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 10_000, "room_id": "!testroomid:example.org", "sender": "@user:example.org", "content": { "body": "test", "msgtype": "m.audio", "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd", "org.matrix.msc1767.text": "test", "org.matrix.msc1767.file": { "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd", }, "org.matrix.msc1767.audio": {}, } }) ); } #[test] fn content_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain( "test".into(), mxc_uri!("mxc://example.org/ffed755USFFxlgbQYZGtryd").to_owned(), None, ))); #[cfg(not(feature = "unstable-msc3246"))] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "test", "msgtype": "m.audio", "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd" }) ); #[cfg(feature = "unstable-msc3246")] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "test", "msgtype": "m.audio", "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd", "org.matrix.msc1767.text": "test", "org.matrix.msc1767.file": { "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd", }, "org.matrix.msc1767.audio": {}, }) ); } #[test] fn custom_msgtype_serialization() { let json_data = json_object! { "custom_field".into() => json!("baba"), "another_one".into() => json!("abab"), }; let custom_msgtype = MessageType::new("my_custom_msgtype", "my message body".into(), json_data).unwrap(); assert_eq!( to_json_value(&custom_msgtype).unwrap(), json!({ "msgtype": "my_custom_msgtype", "body": "my message body", "custom_field": "baba", "another_one": "abab", }) ); } #[test] fn custom_content_deserialization() { let json_data = json!({ "msgtype": "my_custom_msgtype", "body": "my custom message", "custom_field": "baba", "another_one": "abab", }); let expected_json_data = json_object! { "custom_field".into() => json!("baba"), "another_one".into() => json!("abab"), }; let custom_event: MessageType = from_json_value(json_data).unwrap(); assert_eq!(custom_event.msgtype(), "my_custom_msgtype"); assert_eq!(custom_event.body(), "my custom message"); assert_eq!(custom_event.data(), Cow::Owned(expected_json_data)); } #[test] fn formatted_body_serialization() { let message_event_content = RoomMessageEventContent::text_html("Hello, World!", "Hello, World!"); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Hello, World!", "msgtype": "m.text", "format": "org.matrix.custom.html", "formatted_body": "Hello, World!", }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Hello, World!", "msgtype": "m.text", "format": "org.matrix.custom.html", "formatted_body": "Hello, World!", "org.matrix.msc1767.html": "Hello, World!", "org.matrix.msc1767.text": "Hello, World!", }) ); } #[test] fn plain_text_content_serialization() { let message_event_content = RoomMessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "> <@test:example.com> test\n\ntest reply", "msgtype": "m.text" }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "> <@test:example.com> test\n\ntest reply", "msgtype": "m.text", "org.matrix.msc1767.text": "> <@test:example.com> test\n\ntest reply", }) ); } #[test] #[cfg(feature = "markdown")] fn markdown_content_serialization() { use ruma_common::events::room::message::TextMessageEventContent; let formatted_message = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing **bold** and _italic_!"), )); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&formatted_message).unwrap(), json!({ "body": "Testing **bold** and _italic_!", "formatted_body": "

Testing bold and italic!

\n", "format": "org.matrix.custom.html", "msgtype": "m.text" }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(&formatted_message).unwrap(), json!({ "body": "Testing **bold** and _italic_!", "formatted_body": "

Testing bold and italic!

\n", "format": "org.matrix.custom.html", "msgtype": "m.text", "org.matrix.msc1767.html": "

Testing bold and italic!

\n", "org.matrix.msc1767.text": "Testing **bold** and _italic_!", }) ); let plain_message_simple = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing a simple phrase…"), )); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&plain_message_simple).unwrap(), json!({ "body": "Testing a simple phrase…", "msgtype": "m.text" }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(&plain_message_simple).unwrap(), json!({ "body": "Testing a simple phrase…", "msgtype": "m.text", "org.matrix.msc1767.text": "Testing a simple phrase…", }) ); let plain_message_paragraphs = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing\n\nSeveral\n\nParagraphs."), )); #[cfg(not(feature = "unstable-msc1767"))] assert_eq!( to_json_value(&plain_message_paragraphs).unwrap(), json!({ "body": "Testing\n\nSeveral\n\nParagraphs.", "formatted_body": "

Testing

\n

Several

\n

Paragraphs.

\n", "format": "org.matrix.custom.html", "msgtype": "m.text" }) ); #[cfg(feature = "unstable-msc1767")] assert_eq!( to_json_value(&plain_message_paragraphs).unwrap(), json!({ "body": "Testing\n\nSeveral\n\nParagraphs.", "formatted_body": "

Testing

\n

Several

\n

Paragraphs.

\n", "format": "org.matrix.custom.html", "msgtype": "m.text", "org.matrix.msc1767.html": "

Testing

\n

Several

\n

Paragraphs.

\n", "org.matrix.msc1767.text": "Testing\n\nSeveral\n\nParagraphs.", }) ); } #[test] #[cfg(not(feature = "unstable-msc1767"))] fn relates_to_content_serialization() { let message_event_content = assign!(RoomMessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"), { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new( event_id!("$15827405538098VGFWH:example.com").to_owned(), ), }), }); let json_data = json!({ "body": "> <@test:example.com> test\n\ntest reply", "msgtype": "m.text", "m.relates_to": { "m.in_reply_to": { "event_id": "$15827405538098VGFWH:example.com" } } }); assert_eq!(to_json_value(&message_event_content).unwrap(), json_data); } #[test] #[cfg(not(feature = "unstable-msc2676"))] fn edit_deserialization_061() { let json_data = json!({ "body": "s/foo/bar", "msgtype": "m.text", "m.relates_to": { "rel_type": "m.replace", "event_id": "$1598361704261elfgc:localhost", }, "m.new_content": { "body": "bar", }, }); let content = from_json_value::(json_data).unwrap(); assert!(content.relates_to.is_some()); let text = assert_matches!( content.msgtype, MessageType::Text(text) => text ); assert_eq!(text.body, "s/foo/bar"); assert_matches!(text.formatted, None); } #[test] #[cfg(feature = "unstable-msc2676")] fn edit_deserialization_future() { let json_data = json!({ "body": "s/foo/bar", "msgtype": "m.text", "m.relates_to": { "rel_type": "m.replace", "event_id": "$1598361704261elfgc:localhost", }, "m.new_content": { "body": "bar", "msgtype": "m.text", }, }); let content = from_json_value::(json_data).unwrap(); let text = assert_matches!( content.msgtype, MessageType::Text(text) => text ); assert_eq!(text.body, "s/foo/bar"); assert_matches!(text.formatted, None); let replacement = assert_matches!( content.relates_to, Some(Relation::Replacement(replacement)) => replacement ); assert_eq!(replacement.event_id, "$1598361704261elfgc:localhost"); let new_text = assert_matches!( replacement.new_content.msgtype, MessageType::Text(new_text) => new_text ); assert_eq!(new_text.body, "bar"); assert_matches!(new_text.formatted, None); } #[test] fn verification_request_deserialization() { let user_id = user_id!("@example2:localhost"); let device_id: OwnedDeviceId = "XOWLHHFSWM".into(); let json_data = json!({ "body": "@example:localhost is requesting to verify your key, ...", "msgtype": "m.key.verification.request", "to": user_id, "from_device": device_id, "methods": [ "m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1" ] }); let verification = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::VerificationRequest(verification), .. }) => verification ); assert_eq!(verification.body, "@example:localhost is requesting to verify your key, ..."); assert_eq!(verification.to, user_id); assert_eq!(verification.from_device, device_id); assert_eq!(verification.methods.len(), 3); assert!(verification.methods.contains(&VerificationMethod::SasV1)); } #[test] fn verification_request_serialization() { let user_id = user_id!("@example2:localhost").to_owned(); let device_id: OwnedDeviceId = "XOWLHHFSWM".into(); let body = "@example:localhost is requesting to verify your key, ...".to_owned(); let methods = vec![VerificationMethod::SasV1, "m.qr_code.show.v1".into(), "m.reciprocate.v1".into()]; let json_data = json!({ "body": body, "msgtype": "m.key.verification.request", "to": user_id, "from_device": device_id, "methods": methods }); let content = MessageType::VerificationRequest(KeyVerificationRequestEventContent::new( body, methods, device_id, user_id, )); assert_eq!(to_json_value(&content).unwrap(), json_data,); } #[test] fn content_deserialization() { let json_data = json!({ "body": "test", "msgtype": "m.audio", "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd" }); let audio = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Audio(audio), .. }) => audio ); assert_eq!(audio.body, "test"); assert_matches!(audio.info, None); let url = assert_matches!(audio.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://example.org/ffed755USFFxlgbQYZGtryd"); } #[test] fn content_deserialization_failure() { let json_data = json!({ "body": "test","msgtype": "m.location", "url": "http://example.com/audio.mp3" }); assert_matches!(from_json_value::(json_data), Err(_)); } #[test] #[cfg(feature = "unstable-sanitize")] fn reply_sanitize() { use ruma_common::events::room::message::TextMessageEventContent; let first_message = OriginalRoomMessageEvent { content: RoomMessageEventContent::text_html( "# This is the first message", "

This is the first message

", ), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let second_message = OriginalRoomMessageEvent { content: RoomMessageEventContent::text_html( "This is the _second_ message", "This is the second message", ) .make_reply_to(&first_message), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let final_reply = RoomMessageEventContent::text_html( "This is **my** reply", "This is my reply", ) .make_reply_to(&second_message); let (body, formatted) = assert_matches!( first_message.content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!(body, "# This is the first message"); let formatted = formatted.unwrap(); assert_eq!(formatted.body, "

This is the first message

"); let (body, formatted) = assert_matches!( second_message.content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!( body, "\ > <@user:example.org> # This is the first message\n\ This is the _second_ message\ " ); let formatted = formatted.unwrap(); assert_eq!( formatted.body, "\ \
\ \ This is the second message\ " ); let (body, formatted) = assert_matches!( final_reply.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!( body, "\ > <@user:example.org> This is the _second_ message\n\ This is **my** reply\ " ); let formatted = formatted.unwrap(); assert_eq!( formatted.body, "\ \
\ In reply to \ @user:example.org\
\ This is the second message\
\
\ This is my reply\ " ); } ruma-common-0.10.5/tests/events/state_event.rs000064400000000000000000000207361046102023000175060ustar 00000000000000use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ room::aliases::RoomAliasesEventContent, AnyStateEvent, AnyStateEventContent, AnySyncStateEvent, AnyTimelineEvent, OriginalStateEvent, StateEvent, StateEventType, StateUnsigned, SyncStateEvent, }, mxc_uri, room_alias_id, room_id, serde::{CanBeEmpty, Raw}, server_name, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, }; fn aliases_event_with_prev_content() -> JsonValue { json!({ "content": { "aliases": ["#somewhere:localhost"], }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "state_key": "room.com", "type": "m.room.aliases", "unsigned": { "prev_content": { "aliases": ["#inner:localhost"], }, }, }) } #[test] fn serialize_aliases_with_prev_content() { let aliases_event = OriginalStateEvent { content: RoomAliasesEventContent::new(vec![ room_alias_id!("#somewhere:localhost").to_owned() ]), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!roomid:room.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), state_key: server_name!("room.com").to_owned(), unsigned: assign!(StateUnsigned::default(), { prev_content: Some(RoomAliasesEventContent::new(vec![room_alias_id!( "#inner:localhost" ) .to_owned()])), }), }; let actual = to_json_value(&aliases_event).unwrap(); let expected = aliases_event_with_prev_content(); assert_eq!(actual, expected); } #[test] fn serialize_aliases_without_prev_content() { let aliases_event = OriginalStateEvent { content: RoomAliasesEventContent::new(vec![ room_alias_id!("#somewhere:localhost").to_owned() ]), event_id: event_id!("$h29iv0s8:example.com").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), room_id: room_id!("!roomid:room.com").to_owned(), sender: user_id!("@carl:example.com").to_owned(), state_key: server_name!("example.com").to_owned(), unsigned: StateUnsigned::default(), }; let actual = to_json_value(&aliases_event).unwrap(); let expected = json!({ "content": { "aliases": [ "#somewhere:localhost" ] }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "state_key": "example.com", "type": "m.room.aliases", }); assert_eq!(actual, expected); } #[test] fn deserialize_aliases_content() { let json_data = json!({ "aliases": [ "#somewhere:localhost" ] }); let content = assert_matches!( from_json_value::>(json_data) .unwrap() .deserialize_content(StateEventType::RoomAliases), Ok(AnyStateEventContent::RoomAliases(content)) => content ); assert_eq!(content.aliases, vec![room_alias_id!("#somewhere:localhost")]); } #[test] fn deserialize_aliases_with_prev_content() { let json_data = aliases_event_with_prev_content(); let ev = assert_matches!( from_json_value::(json_data), Ok(AnyStateEvent::RoomAliases(StateEvent::Original(ev))) => ev ); assert_eq!(ev.content.aliases, vec![room_alias_id!("#somewhere:localhost")]); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.room_id, "!roomid:room.com"); assert_eq!(ev.sender, "@carl:example.com"); let prev_content = ev.unsigned.prev_content.unwrap(); assert_eq!(prev_content.aliases, vec![room_alias_id!("#inner:localhost")]); } #[test] fn deserialize_aliases_sync_with_room_id() { // The same JSON can be used to create a sync event, it just ignores the `room_id` field let json_data = aliases_event_with_prev_content(); let ev = assert_matches!( from_json_value::(json_data), Ok(AnySyncStateEvent::RoomAliases(SyncStateEvent::Original(ev))) => ev ); assert_eq!(ev.content.aliases, vec![room_alias_id!("#somewhere:localhost")]); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.sender, "@carl:example.com"); let prev_content = ev.unsigned.prev_content.unwrap(); assert_eq!(prev_content.aliases, vec![room_alias_id!("#inner:localhost")]); } #[test] fn deserialize_avatar_without_prev_content() { let json_data = json!({ "content": { "info": { "h": 423, "mimetype": "image/png", "size": 84242, "thumbnail_info": { "h": 334, "mimetype": "image/png", "size": 82595, "w": 800 }, "thumbnail_url": "mxc://matrix.org/98irRSS23srs", "w": 1011 }, "url": "mxc://matrix.org/rnsldl8srs98IRrs" }, "event_id": "$h29iv0s8:example.com", "origin_server_ts": 1, "room_id": "!roomid:room.com", "sender": "@carl:example.com", "state_key": "", "type": "m.room.avatar" }); let ev = assert_matches!( from_json_value::(json_data), Ok(AnyStateEvent::RoomAvatar(StateEvent::Original(ev))) => ev ); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.room_id, "!roomid:room.com"); assert_eq!(ev.sender, "@carl:example.com"); assert!(ev.unsigned.is_empty()); assert_eq!(ev.content.url.as_deref(), Some(mxc_uri!("mxc://matrix.org/rnsldl8srs98IRrs"))); let info = ev.content.info.unwrap(); assert_eq!(info.height, Some(uint!(423))); assert_eq!(info.width, Some(uint!(1011))); assert_eq!(info.mimetype.as_deref(), Some("image/png")); assert_eq!(info.size, Some(uint!(84242))); assert_eq!(info.thumbnail_url.as_deref(), Some(mxc_uri!("mxc://matrix.org/98irRSS23srs"))); let thumbnail_info = info.thumbnail_info.unwrap(); assert_eq!(thumbnail_info.width, Some(uint!(800))); assert_eq!(thumbnail_info.height, Some(uint!(334))); assert_eq!(thumbnail_info.mimetype.as_deref(), Some("image/png")); assert_eq!(thumbnail_info.size, Some(uint!(82595))); } #[test] fn deserialize_member_event_with_top_level_membership_field() { let json_data = json!({ "content": { "avatar_url": null, "displayname": "example", "membership": "join" }, "event_id": "$h29iv0s8:example.com", "membership": "join", "room_id": "!room:localhost", "origin_server_ts": 1, "sender": "@example:localhost", "state_key": "@example:localhost", "type": "m.room.member", "unsigned": { "age": 1, } }); let ev = assert_matches!( from_json_value::(json_data), Ok(AnyTimelineEvent::State(AnyStateEvent::RoomMember(StateEvent::Original(ev)))) => ev ); assert_eq!(ev.event_id, "$h29iv0s8:example.com"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!(ev.sender, "@example:localhost"); assert_eq!(ev.content.displayname.as_deref(), Some("example")); } #[test] fn deserialize_full_event_convert_to_sync() { let json_data = aliases_event_with_prev_content(); let full_ev: AnyStateEvent = from_json_value(json_data).unwrap(); let sync_ev = assert_matches!( AnySyncStateEvent::from(full_ev), AnySyncStateEvent::RoomAliases(SyncStateEvent::Original(ev)) => ev ); assert_eq!(sync_ev.content.aliases, vec![room_alias_id!("#somewhere:localhost")]); assert_eq!(sync_ev.event_id, "$h29iv0s8:example.com"); assert_eq!(sync_ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!( sync_ev.unsigned.prev_content.unwrap().aliases, vec![room_alias_id!("#inner:localhost")] ); assert_eq!(sync_ev.sender, "@carl:example.com"); } ruma-common-0.10.5/tests/events/sticker.rs000064400000000000000000000046611046102023000166300ustar 00000000000000#![cfg(feature = "unstable-msc3552")] use ruma_common::{ events::{room::ImageInfo, sticker::StickerEventContent}, mxc_uri, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn content_serialization() { let message_event_content = StickerEventContent::new( "Upload: my_image.jpg".to_owned(), ImageInfo::new(), mxc_uri!("mxc://notareal.hs/file").to_owned(), ); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_image.jpg", "url": "mxc://notareal.hs/file", "info": {}, "org.matrix.msc1767.text": "Upload: my_image.jpg", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.image": {}, }) ); } #[test] fn content_stable_deserialization() { let json_data = json!({ "body": "Upload: my_image.jpg", "url": "mxc://notareal.hs/file", "info": {}, "m.text": "Upload: my_image.jpg", "m.file": { "url": "mxc://notareal.hs/file", }, "m.image": {}, }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.body, "Upload: my_image.jpg"); assert_eq!(content.url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_image.jpg"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } #[test] fn content_unstable_deserialization() { let json_data = json!({ "body": "Upload: my_image.jpg", "url": "mxc://notareal.hs/file", "info": {}, "org.matrix.msc1767.text": "Upload: my_image.jpg", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.image": {}, }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.body, "Upload: my_image.jpg"); assert_eq!(content.url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_image.jpg"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } ruma-common-0.10.5/tests/events/stripped.rs000064400000000000000000000063771046102023000170240ustar 00000000000000use assert_matches::assert_matches; use js_int::uint; use ruma_common::{ events::{ room::{join_rules::JoinRule, topic::RoomTopicEventContent}, AnyStrippedStateEvent, EmptyStateKey, StrippedStateEvent, }, mxc_uri, user_id, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn serialize_stripped_state_event_any_content() { let event = StrippedStateEvent { content: RoomTopicEventContent::new("Testing room".into()), state_key: EmptyStateKey, sender: user_id!("@example:localhost").to_owned(), }; let json_data = json!({ "content": { "topic": "Testing room" }, "type": "m.room.topic", "state_key": "", "sender": "@example:localhost" }); assert_eq!(to_json_value(&event).unwrap(), json_data); } #[test] fn deserialize_stripped_state_events() { let name_event = json!({ "type": "m.room.name", "state_key": "", "sender": "@example:localhost", "content": { "name": "Ruma" } }); let join_rules_event = json!({ "type": "m.room.join_rules", "state_key": "", "sender": "@example:localhost", "content": { "join_rule": "public" } }); let avatar_event = json!({ "type": "m.room.avatar", "state_key": "", "sender": "@example:localhost", "content": { "info": { "h": 128, "w": 128, "mimetype": "image/jpeg", "size": 1024, "thumbnail_info": { "h": 16, "w": 16, "mimetype": "image/jpeg", "size": 32 }, "thumbnail_url": "mxc://example.com/THumbNa1l" }, "thumbnail_info": { "h": 16, "w": 16, "mimetype": "image/jpeg", "size": 32 }, "thumbnail_url": "mxc://example.com/THumbNa1l", "url": "mxc://example.com/iMag3" } }); let ev = from_json_value::(name_event).unwrap(); let ev = assert_matches!(ev, AnyStrippedStateEvent::RoomName(ev) => ev); assert_eq!(ev.content.name.as_deref(), Some("Ruma")); assert_eq!(ev.sender.to_string(), "@example:localhost"); let ev = from_json_value::(join_rules_event).unwrap(); let ev = assert_matches!(ev,AnyStrippedStateEvent::RoomJoinRules(ev)=>ev ); assert_eq!(ev.content.join_rule, JoinRule::Public); assert_eq!(ev.sender.to_string(), "@example:localhost"); let ev = from_json_value::(avatar_event).unwrap(); let ev = assert_matches!(ev, AnyStrippedStateEvent::RoomAvatar(ev) => ev); assert_eq!(ev.content.url.unwrap(), mxc_uri!("mxc://example.com/iMag3")); assert_eq!(ev.sender.to_string(), "@example:localhost"); let image_info = ev.content.info.unwrap(); assert_eq!(image_info.height, Some(uint!(128))); assert_eq!(image_info.width, Some(uint!(128))); assert_eq!(image_info.mimetype.as_deref(), Some("image/jpeg")); assert_eq!(image_info.size, Some(uint!(1024))); assert_eq!(image_info.thumbnail_info.unwrap().size, Some(uint!(32))); } ruma-common-0.10.5/tests/events/to_device.rs000064400000000000000000000016671046102023000171300ustar 00000000000000use ruma_common::{ events::{room_key::ToDeviceRoomKeyEventContent, ToDeviceEvent}, room_id, user_id, EventEncryptionAlgorithm, }; use serde_json::{json, to_value as to_json_value}; #[test] fn serialization() { let ev = ToDeviceEvent { sender: user_id!("@example:example.org").to_owned(), content: ToDeviceRoomKeyEventContent::new( EventEncryptionAlgorithm::MegolmV1AesSha2, room_id!("!testroomid:example.org").to_owned(), "SessId".into(), "SessKey".into(), ), }; assert_eq!( to_json_value(&ev).unwrap(), json!({ "type": "m.room_key", "sender": "@example:example.org", "content": { "algorithm": "m.megolm.v1.aes-sha2", "room_id": "!testroomid:example.org", "session_id": "SessId", "session_key": "SessKey", }, }) ); } ruma-common-0.10.5/tests/events/ui/01-content-sanity-check.rs000064400000000000000000000004221046102023000220400ustar 00000000000000use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[ruma_event(type = "m.macro.test", kind = State, state_key_type = String)] pub struct MacroTestContent { pub url: String, } fn main() {} ruma-common-0.10.5/tests/events/ui/02-no-event-type.rs000064400000000000000000000002771046102023000205310ustar 00000000000000use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] pub struct MacroTest { pub url: String, } fn main() {} ruma-common-0.10.5/tests/events/ui/02-no-event-type.stderr000064400000000000000000000006561046102023000214110ustar 00000000000000error: no event type attribute found, add `#[ruma_event(type = "any.room.event", kind = Kind)]` below the event content derive --> $DIR/02-no-event-type.rs:4:48 | 4 | #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] | ^^^^^^^^^^^^ | = note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info) ruma-common-0.10.5/tests/events/ui/03-invalid-event-type.rs000064400000000000000000000006351046102023000215420ustar 00000000000000use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[not_ruma_event(type = "m.macro.test", kind = State)] pub struct MacroTest { pub test: String, } #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[ruma_event(event = "m.macro.test", kind = State)] pub struct MoreMacroTest { pub test: String, } fn main() {} ruma-common-0.10.5/tests/events/ui/03-invalid-event-type.stderr000064400000000000000000000016741046102023000224250ustar 00000000000000error: no event type attribute found, add `#[ruma_event(type = "any.room.event", kind = Kind)]` below the event content derive --> tests/events/ui/03-invalid-event-type.rs:4:48 | 4 | #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] | ^^^^^^^^^^^^ | = note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected one of: `type`, `kind`, `custom_redacted`, `state_key_type`, `unsigned_type`, `alias` --> tests/events/ui/03-invalid-event-type.rs:11:14 | 11 | #[ruma_event(event = "m.macro.test", kind = State)] | ^^^^^ error: cannot find attribute `not_ruma_event` in this scope --> tests/events/ui/03-invalid-event-type.rs:5:3 | 5 | #[not_ruma_event(type = "m.macro.test", kind = State)] | ^^^^^^^^^^^^^^ help: a derive helper attribute with a similar name exists: `ruma_event` ruma-common-0.10.5/tests/events/ui/04-event-sanity-check.rs000064400000000000000000000012221046102023000215110ustar 00000000000000// Required, probably until Rust 1.57 // https://github.com/rust-lang/rust/issues/55779 #[allow(unused_extern_crates)] extern crate serde; use ruma_common::{ events::{StateEventContent, StateUnsigned}, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, }; use ruma_macros::Event; /// State event. #[derive(Clone, Debug, Event)] pub struct OriginalStateEvent { pub content: C, pub event_id: OwnedEventId, pub sender: OwnedUserId, pub origin_server_ts: MilliSecondsSinceUnixEpoch, pub room_id: OwnedRoomId, pub state_key: C::StateKey, pub unsigned: StateUnsigned, } fn main() {} ruma-common-0.10.5/tests/events/ui/05-named-fields.rs000064400000000000000000000002731046102023000203460ustar 00000000000000use ruma_common::events::StateEventContent; use ruma_macros::Event; /// State event. #[derive(Clone, Debug, Event)] pub struct OriginalStateEvent(C); fn main() {} ruma-common-0.10.5/tests/events/ui/05-named-fields.stderr000064400000000000000000000003211046102023000212170ustar 00000000000000error: the `Event` derive only supports structs with named fields --> tests/events/ui/05-named-fields.rs:6:12 | 6 | pub struct OriginalStateEvent(C); | ^^^^^^^^^^^^^^^^^^ ruma-common-0.10.5/tests/events/ui/06-no-content-field.rs000064400000000000000000000003231046102023000211600ustar 00000000000000use ruma_common::events::StateEventContent; use ruma_macros::Event; /// State event. #[derive(Clone, Debug, Event)] pub struct OriginalStateEvent { pub not_content: C, } fn main() {} ruma-common-0.10.5/tests/events/ui/06-no-content-field.stderr000064400000000000000000000004451046102023000220440ustar 00000000000000error: struct must contain a `content` field --> tests/events/ui/06-no-content-field.rs:5:24 | 5 | #[derive(Clone, Debug, Event)] | ^^^^^ | = note: this error originates in the derive macro `Event` (in Nightly builds, run with -Z macro-backtrace for more info) ruma-common-0.10.5/tests/events/ui/07-enum-sanity-check.rs000064400000000000000000000026301046102023000213430ustar 00000000000000use ruma_common::events; use ruma_macros::event_enum; event_enum! { /// Any global account data event. enum GlobalAccountData { #[ruma_enum(alias = "io.ruma.direct")] "m.direct" => events::direct, #[ruma_enum(alias = "m.identity_server")] "io.ruma.identity_server" => events::identity_server, #[cfg(test)] "m.ignored_user_list" => events::ignored_user_list, // Doesn't actually have a wildcard, but this should work as a wildcard test "m.push_rules.*" => events::push_rules, #[cfg(any())] "m.ruma_test" => events::ruma_test, } } fn main() { assert_eq!(GlobalAccountDataEventType::from("m.direct"), GlobalAccountDataEventType::Direct); assert_eq!( GlobalAccountDataEventType::from("io.ruma.direct"), GlobalAccountDataEventType::Direct ); assert_eq!(GlobalAccountDataEventType::Direct.to_cow_str(), "m.direct"); assert_eq!( GlobalAccountDataEventType::from("m.identity_server"), GlobalAccountDataEventType::IdentityServer ); assert_eq!( GlobalAccountDataEventType::from("io.ruma.identity_server"), GlobalAccountDataEventType::IdentityServer ); assert_eq!(GlobalAccountDataEventType::IdentityServer.to_cow_str(), "io.ruma.identity_server"); } #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PrivOwnedStr(Box); ruma-common-0.10.5/tests/events/ui/08-enum-invalid-path.rs000064400000000000000000000004151046102023000213410ustar 00000000000000use ruma_common::events; use ruma_macros::event_enum; event_enum! { enum State { "m.not.a.path" => events::not::a::path, } } fn main() {} #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PrivOwnedStr(Box); ruma-common-0.10.5/tests/events/ui/08-enum-invalid-path.stderr000064400000000000000000000003671046102023000222260ustar 00000000000000error[E0433]: failed to resolve: could not find `not` in `events` --> tests/events/ui/08-enum-invalid-path.rs:6:35 | 6 | "m.not.a.path" => events::not::a::path, | ^^^ could not find `not` in `events` ruma-common-0.10.5/tests/events/ui/09-enum-invalid-kind.rs000064400000000000000000000003111046102023000213260ustar 00000000000000use ruma_macros::event_enum; event_enum! { enum NotReal { "m.direct", "m.dummy", "m.ignored_user_list", "m.push_rules", "m.room_key", } } fn main() {} ruma-common-0.10.5/tests/events/ui/09-enum-invalid-kind.stderr000064400000000000000000000003241046102023000222110ustar 00000000000000error: valid event kinds are GlobalAccountData, RoomAccountData, EphemeralRoom, MessageLike, State, ToDevice found `NotReal` --> $DIR/09-enum-invalid-kind.rs:4:10 | 4 | enum NotReal { | ^^^^^^^ ruma-common-0.10.5/tests/events/ui/10-content-wildcard.rs000064400000000000000000000007261046102023000212560ustar 00000000000000use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[ruma_event(type = "m.macro.test.*", kind = GlobalAccountData)] pub struct MacroTestContent { #[ruma_event(type_fragment)] pub frag: String, } fn main() { use ruma_common::events::EventContent; assert_eq!( MacroTestContent { frag: "foo".to_owned() }.event_type().to_string(), "m.macro.test.foo" ); } ruma-common-0.10.5/tests/events/video.rs000064400000000000000000000330451046102023000162700ustar 00000000000000#![cfg(feature = "unstable-msc3553")] use std::time::Duration; use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ file::{EncryptedContentInit, FileContent, FileContentInfo}, image::{ThumbnailContent, ThumbnailFileContent, ThumbnailFileContentInfo}, message::MessageContent, room::{ message::{ InReplyTo, MessageType, Relation, RoomMessageEventContent, VideoMessageEventContent, }, JsonWebKeyInit, MediaSource, }, video::{VideoContent, VideoEventContent}, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, mxc_uri, room_id, serde::{Base64, CanBeEmpty}, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn plain_content_serialization() { let event_content = VideoEventContent::plain( "Upload: my_video.webm", FileContent::plain(mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), None), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_video.webm", "m.file": { "url": "mxc://notareal.hs/abcdef", }, "m.video": {} }) ); } #[test] fn encrypted_content_serialization() { let event_content = VideoEventContent::plain( "Upload: my_video.webm", FileContent::encrypted( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), EncryptedContentInit { key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), None, ), ); assert_eq!( to_json_value(&event_content).unwrap(), json!({ "org.matrix.msc1767.text": "Upload: my_video.webm", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" }, "m.video": {} }) ); } #[test] fn event_serialization() { let event = OriginalMessageLikeEvent { content: assign!( VideoEventContent::with_message( MessageContent::html( "Upload: my_lava_lamp.webm", "Upload: my_lava_lamp.webm", ), FileContent::plain( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), Some(Box::new(assign!( FileContentInfo::new(), { name: Some("my_lava_lamp.webm".to_owned()), mimetype: Some("video/webm".to_owned()), size: Some(uint!(1_897_774)), } ))), ) ), { video: Box::new(assign!( VideoContent::new(), { width: Some(uint!(1920)), height: Some(uint!(1080)), duration: Some(Duration::from_secs(15)), } )), thumbnail: vec![ThumbnailContent::new( ThumbnailFileContent::plain( mxc_uri!("mxc://notareal.hs/thumbnail").to_owned(), Some(Box::new(assign!(ThumbnailFileContentInfo::new(), { mimetype: Some("image/jpeg".to_owned()), size: Some(uint!(334_593)), }))) ), None )], caption: Some(MessageContent::plain("This is my awesome vintage lava lamp")), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), }), } ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.html": "Upload: my_lava_lamp.webm", "org.matrix.msc1767.text": "Upload: my_lava_lamp.webm", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_lava_lamp.webm", "mimetype": "video/webm", "size": 1_897_774, }, "m.video": { "width": 1920, "height": 1080, "duration": 15_000, }, "m.thumbnail": [ { "url": "mxc://notareal.hs/thumbnail", "mimetype": "image/jpeg", "size": 334_593, } ], "m.caption": [ { "body": "This is my awesome vintage lava lamp", "mimetype": "text/plain", } ], "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com" } } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.video", }) ); } #[test] fn plain_content_deserialization() { let json_data = json!({ "m.text": "Video: my_cat.mp4", "m.file": { "url": "mxc://notareal.hs/abcdef", }, "m.video": { "duration": 5_668, }, "m.caption": [ { "body": "Look at my cat!", } ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Video: my_cat.mp4")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert_matches!(content.file.encryption_info, None); assert_eq!(content.video.width, None); assert_eq!(content.video.height, None); assert_eq!(content.video.duration, Some(Duration::from_millis(5_668))); assert_eq!(content.thumbnail.len(), 0); let caption = content.caption.unwrap(); assert_eq!(caption.find_plain(), Some("Look at my cat!")); assert_eq!(caption.find_html(), None); } #[test] fn encrypted_content_deserialization() { let json_data = json!({ "m.text": "Video: my_cat.mp4", "m.file": { "url": "mxc://notareal.hs/abcdef", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2" }, "m.video": {}, "m.thumbnail": [ { "url": "mxc://notareal.hs/thumbnail", } ] }); let content = from_json_value::(json_data).unwrap(); assert_eq!(content.message.find_plain(), Some("Video: my_cat.mp4")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert!(content.file.encryption_info.is_some()); assert_eq!(content.video.width, None); assert_eq!(content.video.height, None); assert_eq!(content.video.duration, None); assert_eq!(content.thumbnail.len(), 1); assert_eq!(content.thumbnail[0].file.url, "mxc://notareal.hs/thumbnail"); assert_matches!(content.caption, None); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "m.text": "Upload: my_gnome.webm", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "my_gnome.webm", "mimetype": "video/webm", "size": 123_774, }, "m.video": { "width": 1300, "height": 837, } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.video", }); let ev = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Video(MessageLikeEvent::Original(ev))) => ev ); assert_eq!(ev.event_id, "$event:notareal.hs"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(ev.room_id, "!roomid:notareal.hs"); assert_eq!(ev.sender, "@user:notareal.hs"); assert!(ev.unsigned.is_empty()); let content = ev.content; assert_eq!(content.message.find_plain(), Some("Upload: my_gnome.webm")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert_eq!(content.video.width, Some(uint!(1300))); assert_eq!(content.video.height, Some(uint!(837))); assert_eq!(content.video.duration, None); assert_eq!(content.thumbnail.len(), 0); let info = content.file.info.unwrap(); assert_eq!(info.name.as_deref(), Some("my_gnome.webm")); assert_eq!(info.mimetype.as_deref(), Some("video/webm")); assert_eq!(info.size, Some(uint!(123_774))); } #[test] fn room_message_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Video(VideoMessageEventContent::plain( "Upload: my_video.mp4".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_video.mp4", "url": "mxc://notareal.hs/file", "msgtype": "m.video", "org.matrix.msc1767.text": "Upload: my_video.mp4", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.video": {}, }) ); } #[test] fn room_message_stable_deserialization() { let json_data = json!({ "body": "Upload: my_video.mp4", "url": "mxc://notareal.hs/file", "msgtype": "m.video", "m.text": "Upload: my_video.mp4", "m.file": { "url": "mxc://notareal.hs/file", }, "m.video": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Video(content) => content); assert_eq!(content.body, "Upload: my_video.mp4"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_video.mp4"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } #[test] fn room_message_unstable_deserialization() { let json_data = json!({ "body": "Upload: my_video.mp4", "url": "mxc://notareal.hs/file", "msgtype": "m.video", "org.matrix.msc1767.text": "Upload: my_video.mp4", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.video": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Video(content) => content); assert_eq!(content.body, "Upload: my_video.mp4"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: my_video.mp4"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); } ruma-common-0.10.5/tests/events/voice.rs000064400000000000000000000172111046102023000162640ustar 00000000000000#![cfg(feature = "unstable-msc3245")] use std::time::Duration; use assert_matches::assert_matches; use assign::assign; use js_int::uint; use ruma_common::{ event_id, events::{ audio::AudioContent, file::{FileContent, FileContentInfo}, room::{ message::{ AudioMessageEventContent, InReplyTo, MessageType, Relation, RoomMessageEventContent, }, MediaSource, }, voice::{VoiceContent, VoiceEventContent}, AnyMessageLikeEvent, MessageLikeEvent, MessageLikeUnsigned, OriginalMessageLikeEvent, }, mxc_uri, room_id, serde::CanBeEmpty, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[test] fn event_serialization() { let event = OriginalMessageLikeEvent { content: assign!( VoiceEventContent::plain( "Voice message", FileContent::plain( mxc_uri!("mxc://notareal.hs/abcdef").to_owned(), Some(Box::new(assign!( FileContentInfo::new(), { name: Some("voice_message.ogg".to_owned()), mimetype: Some("audio/opus".to_owned()), size: Some(uint!(897_774)), } ))), ) ), { audio: assign!( AudioContent::new(), { duration: Some(Duration::from_secs(23)) } ), relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), }), } ), event_id: event_id!("$event:notareal.hs").to_owned(), sender: user_id!("@user:notareal.hs").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)), room_id: room_id!("!roomid:notareal.hs").to_owned(), unsigned: MessageLikeUnsigned::default(), }; assert_eq!( to_json_value(&event).unwrap(), json!({ "content": { "org.matrix.msc1767.text": "Voice message", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "voice_message.ogg", "mimetype": "audio/opus", "size": 897_774, }, "m.audio": { "duration": 23_000, }, "m.voice": {}, "m.relates_to": { "m.in_reply_to": { "event_id": "$replyevent:example.com" } } }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.voice", }) ); } #[test] fn message_event_deserialization() { let json_data = json!({ "content": { "m.text": "Voice message", "m.file": { "url": "mxc://notareal.hs/abcdef", "name": "voice_message.ogg", "mimetype": "audio/opus", "size": 123_774, }, "m.audio": { "duration": 5_300, }, "m.voice": {}, }, "event_id": "$event:notareal.hs", "origin_server_ts": 134_829_848, "room_id": "!roomid:notareal.hs", "sender": "@user:notareal.hs", "type": "m.voice", }); let ev = assert_matches!( from_json_value::(json_data), Ok(AnyMessageLikeEvent::Voice(MessageLikeEvent::Original(ev))) => ev ); assert_eq!(ev.event_id, "$event:notareal.hs"); assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(134_829_848))); assert_eq!(ev.room_id, "!roomid:notareal.hs"); assert_eq!(ev.sender, "@user:notareal.hs"); assert!(ev.unsigned.is_empty()); let content = ev.content; assert_eq!(content.message.find_plain(), Some("Voice message")); assert_eq!(content.message.find_html(), None); assert_eq!(content.file.url, "mxc://notareal.hs/abcdef"); assert_eq!(content.audio.duration, Some(Duration::from_millis(5_300))); assert_matches!(content.audio.waveform, None); let info = content.file.info.unwrap(); assert_eq!(info.name.as_deref(), Some("voice_message.ogg")); assert_eq!(info.mimetype.as_deref(), Some("audio/opus")); assert_eq!(info.size, Some(uint!(123_774))); } #[test] fn room_message_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Audio(assign!( AudioMessageEventContent::plain( "Upload: voice_message.ogg".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ), { voice: Some(VoiceContent::new()), } ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: voice_message.ogg", "url": "mxc://notareal.hs/file", "msgtype": "m.audio", "org.matrix.msc1767.text": "Upload: voice_message.ogg", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.audio": {}, "org.matrix.msc3245.voice": {}, }) ); } #[test] fn room_message_stable_deserialization() { let json_data = json!({ "body": "Upload: voice_message.ogg", "url": "mxc://notareal.hs/file", "msgtype": "m.audio", "m.text": "Upload: voice_message.ogg", "m.file": { "url": "mxc://notareal.hs/file", }, "m.audio": {}, "m.voice": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Audio(content) => content); assert_eq!(content.body, "Upload: voice_message.ogg"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: voice_message.ogg"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); assert!(content.voice.is_some()); } #[test] fn room_message_unstable_deserialization() { let json_data = json!({ "body": "Upload: voice_message.ogg", "url": "mxc://notareal.hs/file", "msgtype": "m.audio", "org.matrix.msc1767.text": "Upload: voice_message.ogg", "org.matrix.msc1767.file": { "url": "mxc://notareal.hs/file", }, "org.matrix.msc1767.audio": {}, "org.matrix.msc3245.voice": {}, }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Audio(content) => content); assert_eq!(content.body, "Upload: voice_message.ogg"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); let message = content.message.unwrap(); assert_eq!(message.len(), 1); assert_eq!(message[0].body, "Upload: voice_message.ogg"); let file = content.file.unwrap(); assert_eq!(file.url, "mxc://notareal.hs/file"); assert!(!file.is_encrypted()); assert!(content.voice.is_some()); } ruma-common-0.10.5/tests/identifiers/id_macros.rs000064400000000000000000000004021046102023000201120ustar 00000000000000#[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"); } ruma-common-0.10.5/tests/identifiers/mod.rs000064400000000000000000000000171046102023000167330ustar 00000000000000mod id_macros; ruma-common-0.10.5/tests/identifiers/ui/01-valid-id-macros.rs000064400000000000000000000012771046102023000217730ustar 00000000000000fn main() { let _ = ruma_common::device_key_id!("ed25519:JLAFKJWSCS"); let _ = ruma_common::event_id!("$39hvsi03hlne:example.com"); let _ = ruma_common::event_id!("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"); let _ = ruma_common::mxc_uri!("mxc://myserver.fish/sdfdsfsdfsdfgsdfsd"); let _ = ruma_common::room_alias_id!("#alias:server.tld"); let _ = ruma_common::room_id!("!1234567890:matrix.org"); let _ = ruma_common::room_version_id!("1"); let _ = ruma_common::room_version_id!("1-custom"); let _ = ruma_common::server_signing_key_id!("ed25519:Abc_1"); let _ = ruma_common::server_name!("myserver.fish"); let _ = ruma_common::user_id!("@user:ruma.io"); } ruma-common-0.10.5/tests/identifiers/ui/02-invalid-id-macros.rs000064400000000000000000000007161046102023000223200ustar 00000000000000fn main() { let _ = ruma_common::event_id!("39hvsi03hlne:example.com"); let _ = ruma_common::event_id!("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"); let _ = ruma_common::mxc_uri!(""); let _ = ruma_common::room_alias_id!("alias:server.tld"); let _ = ruma_common::room_id!("1234567890:matrix.org"); let _ = ruma_common::room_version_id!(""); let _ = ruma_common::server_name!(""); let _ = ruma_common::user_id!("user:ruma.io"); } ruma-common-0.10.5/tests/identifiers/ui/02-invalid-id-macros.stderr000064400000000000000000000055461046102023000232050ustar 00000000000000error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:2:13 | 2 | let _ = ruma_common::event_id!("39hvsi03hlne:example.com"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid event id = note: this error originates in the macro `ruma_common::event_id` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:3:13 | 3 | let _ = ruma_common::event_id!("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid event id = note: this error originates in the macro `ruma_common::event_id` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:4:13 | 4 | let _ = ruma_common::mxc_uri!(""); | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid mxc:// = note: this error originates in the macro `ruma_common::mxc_uri` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:5:13 | 5 | let _ = ruma_common::room_alias_id!("alias:server.tld"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid room_alias_id = note: this error originates in the macro `ruma_common::room_alias_id` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:6:13 | 6 | let _ = ruma_common::room_id!("1234567890:matrix.org"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid room_id = note: this error originates in the macro `ruma_common::room_id` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:7:13 | 7 | let _ = ruma_common::room_version_id!(""); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid room_version_id = note: this error originates in the macro `ruma_common::room_version_id` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:8:13 | 8 | let _ = ruma_common::server_name!(""); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid server_name = note: this error originates in the macro `ruma_common::server_name` (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:9:13 | 9 | let _ = ruma_common::user_id!("user:ruma.io"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid user_id = note: this error originates in the macro `ruma_common::user_id` (in Nightly builds, run with -Z macro-backtrace for more info) ruma-common-0.10.5/tests/identifiers/ui/03-invalid-new-id-macros.rs000064400000000000000000000001001046102023000230730ustar 00000000000000fn main() { let _ = ruma_common::session_id!("invalid~"); } ruma-common-0.10.5/tests/identifiers/ui/03-invalid-new-id-macros.stderr000064400000000000000000000007701046102023000237670ustar 00000000000000error[E0080]: evaluation of constant value failed --> tests/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 | = note: this error originates in the macro `$crate::panic::panic_2021` (in Nightly builds, run with -Z macro-backtrace for more info) ruma-common-0.10.5/tests/serde/empty_strings.rs000064400000000000000000000060221046102023000176620ustar 00000000000000mod string { use serde::{Deserialize, Serialize}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[derive(Serialize, Deserialize, PartialEq, Debug)] struct StringStruct { #[serde( default, deserialize_with = "ruma_common::serde::empty_string_as_none", serialize_with = "ruma_common::serde::none_as_empty_string" )] x: Option, } #[test] fn none_se() { let decoded = StringStruct { x: None }; let encoded = json!({ "x": "" }); assert_eq!(to_json_value(decoded).unwrap(), encoded); } #[test] fn some_se() { let decoded = StringStruct { x: Some("foo".into()) }; let encoded = json!({ "x": "foo" }); assert_eq!(to_json_value(decoded).unwrap(), encoded); } #[test] fn absent_de() { let encoded = json!({}); let decoded = StringStruct { x: None }; assert_eq!(from_json_value::(encoded).unwrap(), decoded); } #[test] fn empty_de() { let encoded = json!({ "x": "" }); let decoded = StringStruct { x: None }; assert_eq!(from_json_value::(encoded).unwrap(), decoded); } #[test] fn some_de() { let encoded = json!({ "x": "foo" }); let decoded = StringStruct { x: Some("foo".into()) }; assert_eq!(from_json_value::(encoded).unwrap(), decoded); } } mod user { use ruma_common::{user_id, OwnedUserId, UserId}; use serde::{Deserialize, Serialize}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; const CARL: &str = "@carl:example.com"; fn carl() -> &'static UserId { user_id!("@carl:example.com") } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct User { #[serde( default, deserialize_with = "ruma_common::serde::empty_string_as_none", serialize_with = "ruma_common::serde::none_as_empty_string" )] x: Option, } #[test] fn none_se() { let decoded = User { x: None }; let encoded = json!({ "x": "" }); assert_eq!(to_json_value(decoded).unwrap(), encoded); } #[test] fn some_se() { let decoded = User { x: Some(carl().to_owned()) }; let encoded = json!({ "x": CARL }); assert_eq!(to_json_value(decoded).unwrap(), encoded); } #[test] fn absent_de() { let encoded = json!({}); let decoded = User { x: None }; assert_eq!(from_json_value::(encoded).unwrap(), decoded); } #[test] fn empty_de() { let encoded = json!({ "x": "" }); let decoded = User { x: None }; assert_eq!(from_json_value::(encoded).unwrap(), decoded); } #[test] fn some_de() { let encoded = json!({ "x": CARL }); let decoded = User { x: Some(carl().to_owned()) }; assert_eq!(from_json_value::(encoded).unwrap(), decoded); } } ruma-common-0.10.5/tests/serde/enum_derive.rs000064400000000000000000000054631046102023000172650ustar 00000000000000use ruma_common::serde::StringEnum; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; #[derive(Debug, PartialEq)] struct PrivOwnedStr(Box); #[derive(Debug, PartialEq, StringEnum)] #[ruma_enum(rename_all = "snake_case")] enum MyEnum { First, Second, #[ruma_enum(rename = "m.third")] Third, HelloWorld, #[ruma_enum(rename = "io.ruma.unstable", alias = "m.stable", alias = "hs.notareal.unstable")] Stable, _Custom(PrivOwnedStr), } #[test] fn as_ref_str() { assert_eq!(MyEnum::First.as_ref(), "first"); assert_eq!(MyEnum::Second.as_ref(), "second"); assert_eq!(MyEnum::Third.as_ref(), "m.third"); assert_eq!(MyEnum::HelloWorld.as_ref(), "hello_world"); assert_eq!(MyEnum::Stable.as_ref(), "io.ruma.unstable"); assert_eq!(MyEnum::_Custom(PrivOwnedStr("HelloWorld".into())).as_ref(), "HelloWorld"); } #[test] fn display() { assert_eq!(MyEnum::First.to_string(), "first"); assert_eq!(MyEnum::Second.to_string(), "second"); assert_eq!(MyEnum::Third.to_string(), "m.third"); assert_eq!(MyEnum::HelloWorld.to_string(), "hello_world"); assert_eq!(MyEnum::Stable.to_string(), "io.ruma.unstable"); assert_eq!(MyEnum::_Custom(PrivOwnedStr("HelloWorld".into())).to_string(), "HelloWorld"); } #[test] fn from_string() { assert_eq!(MyEnum::from("first"), MyEnum::First); assert_eq!(MyEnum::from("second"), MyEnum::Second); assert_eq!(MyEnum::from("m.third"), MyEnum::Third); assert_eq!(MyEnum::from("hello_world"), MyEnum::HelloWorld); assert_eq!(MyEnum::from("io.ruma.unstable"), MyEnum::Stable); assert_eq!(MyEnum::from("m.stable"), MyEnum::Stable); assert_eq!(MyEnum::from("hs.notareal.unstable"), MyEnum::Stable); assert_eq!(MyEnum::from("HelloWorld"), MyEnum::_Custom(PrivOwnedStr("HelloWorld".into()))); } #[test] fn serialize() { assert_eq!(to_json_value(MyEnum::First).unwrap(), json!("first")); assert_eq!(to_json_value(MyEnum::HelloWorld).unwrap(), json!("hello_world")); assert_eq!(to_json_value(MyEnum::Stable).unwrap(), json!("io.ruma.unstable")); assert_eq!( to_json_value(MyEnum::_Custom(PrivOwnedStr("\\\n\\".into()))).unwrap(), json!("\\\n\\") ); } #[test] fn deserialize() { assert_eq!(from_json_value::(json!("first")).unwrap(), MyEnum::First); assert_eq!(from_json_value::(json!("hello_world")).unwrap(), MyEnum::HelloWorld); assert_eq!(from_json_value::(json!("io.ruma.unstable")).unwrap(), MyEnum::Stable); assert_eq!(from_json_value::(json!("m.stable")).unwrap(), MyEnum::Stable); assert_eq!(from_json_value::(json!("hs.notareal.unstable")).unwrap(), MyEnum::Stable); assert_eq!( from_json_value::(json!("\\\n\\")).unwrap(), MyEnum::_Custom(PrivOwnedStr("\\\n\\".into())) ); } ruma-common-0.10.5/tests/serde/mod.rs000064400000000000000000000001141046102023000155260ustar 00000000000000mod empty_strings; mod enum_derive; mod url_deserialize; mod url_serialize; ruma-common-0.10.5/tests/serde/url_deserialize.rs000064400000000000000000000147751046102023000201530ustar 00000000000000use assert_matches::assert_matches; use form_urlencoded::Serializer as Encoder; use ruma_common::serde::urlencoded; use serde::Deserialize; #[derive(Deserialize, Debug, PartialEq)] struct NewType(T); #[test] fn deserialize_newtype_i32() { let result = vec![("field".to_owned(), NewType(11))]; assert_eq!(urlencoded::from_str("field=11"), Ok(result)); } #[test] fn deserialize_bytes() { let result = vec![("first".to_owned(), 23), ("last".to_owned(), 42)]; assert_eq!(urlencoded::from_bytes(b"first=23&last=42"), Ok(result)); } #[test] fn deserialize_str() { let result = vec![("first".to_owned(), 23), ("last".to_owned(), 42)]; assert_eq!(urlencoded::from_str("first=23&last=42"), Ok(result)); } #[test] fn deserialize_borrowed_str() { let result = vec![("first", 23), ("last", 42)]; assert_eq!(urlencoded::from_str("first=23&last=42"), Ok(result)); } #[test] fn deserialize_reader() { let result = vec![("first".to_owned(), 23), ("last".to_owned(), 42)]; assert_eq!(urlencoded::from_reader(b"first=23&last=42" as &[_]), Ok(result)); } #[test] fn deserialize_option() { let result = vec![("first".to_owned(), Some(23)), ("last".to_owned(), Some(42))]; assert_eq!(urlencoded::from_str("first=23&last=42"), Ok(result)); } #[test] fn deserialize_unit() { assert_eq!(urlencoded::from_str(""), Ok(())); assert_eq!(urlencoded::from_str("&"), Ok(())); assert_eq!(urlencoded::from_str("&&"), Ok(())); urlencoded::from_str::<()>("first=23").unwrap_err(); } #[derive(Deserialize, Debug, PartialEq, Eq)] enum X { A, B, C, } #[test] fn deserialize_unit_enum() { let result: Vec<(String, X)> = urlencoded::from_str("one=A&two=B&three=C").unwrap(); assert_eq!(result.len(), 3); assert!(result.contains(&("one".to_owned(), X::A))); assert!(result.contains(&("two".to_owned(), X::B))); assert!(result.contains(&("three".to_owned(), X::C))); } #[test] fn deserialize_unit_type() { assert_eq!(urlencoded::from_str(""), Ok(())); } #[derive(Clone, Copy, Debug, PartialEq, Deserialize)] struct Params<'a> { a: usize, b: &'a str, c: Option, } #[test] fn deserialize_struct() { let de = Params { a: 10, b: "Hello", c: None }; assert_eq!(urlencoded::from_str("a=10&b=Hello"), Ok(de)); assert_eq!(urlencoded::from_str("b=Hello&a=10"), Ok(de)); assert_eq!(urlencoded::from_str("a=10&b=Hello&d=1&d=2"), Ok(de)); } #[test] fn deserialize_list_of_str() { // TODO: It would make sense to support this. assert_matches!( urlencoded::from_str::>("a=a&a=b"), Err(error) if error.to_string().contains("unsupported") ); assert_eq!(urlencoded::from_str("a=a&a=b"), Ok(vec![("a", vec!["a", "b"])])); } #[test] fn deserialize_multiple_lists() { #[derive(Debug, PartialEq, Deserialize)] struct Lists { xs: Vec, ys: Vec, } assert_eq!( urlencoded::from_str("xs=true&xs=false&ys=3&ys=2&ys=1"), Ok(Lists { xs: vec![true, false], ys: vec![3, 2, 1] }) ); assert_eq!( urlencoded::from_str("ys=3&xs=true&ys=2&xs=false&ys=1"), Ok(Lists { xs: vec![true, false], ys: vec![3, 2, 1] }) ); } #[test] fn deserialize_with_serde_attributes() { #[derive(Debug, PartialEq, Deserialize)] struct FieldsWithAttributes { #[serde(default)] xs: Vec, #[serde(default)] def: Option, #[serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")] str: Option, #[serde(default)] flag: bool, } assert_eq!( urlencoded::from_str("xs=true&xs=false&def=3&str=&flag=true"), Ok(FieldsWithAttributes { xs: vec![true, false], def: Some(3), str: None, flag: true }) ); assert_eq!( urlencoded::from_str(""), Ok(FieldsWithAttributes { xs: vec![], def: None, str: None, flag: false }) ); } #[test] fn deserialize_nested_list() { urlencoded::from_str::>)>>("a=b").unwrap_err(); } #[test] fn deserialize_list_of_option() { assert_eq!( urlencoded::from_str("list=10&list=100"), Ok(vec![("list", vec![Some(10), Some(100)])]) ); } #[test] fn deserialize_list_of_newtype() { assert_eq!(urlencoded::from_str("list=test"), Ok(vec![("list", vec![NewType("test")])])); } #[test] fn deserialize_list_of_enum() { assert_eq!( urlencoded::from_str("item=A&item=B&item=C"), Ok(vec![("item", vec![X::A, X::B, X::C])]) ); } #[derive(Debug, Deserialize, PartialEq)] struct Wrapper { item: T, } #[derive(Debug, PartialEq, Deserialize)] struct NewStruct<'a> { #[serde(borrow)] list: Vec<&'a str>, } #[derive(Debug, PartialEq, Deserialize)] struct Struct<'a> { #[serde(borrow)] list: Vec>, } #[derive(Debug, PartialEq, Deserialize)] struct NumList { list: Vec, } #[derive(Debug, PartialEq, Deserialize)] struct ListStruct { list: Vec>, } #[test] fn deserialize_newstruct() { let de = NewStruct { list: vec!["hello", "world"] }; assert_eq!(urlencoded::from_str("list=hello&list=world"), Ok(de)); } #[test] fn deserialize_numlist() { let de = NumList { list: vec![1, 2, 3, 4] }; assert_eq!(urlencoded::from_str("list=1&list=2&list=3&list=4"), Ok(de)); } #[derive(Debug, Deserialize, PartialEq)] struct Nested { item: T, } #[derive(Debug, Deserialize, PartialEq)] struct Inner<'a> { c: &'a str, a: usize, b: &'a str, } #[derive(Debug, Deserialize, PartialEq)] struct InnerList { list: Vec, } #[test] #[ignore] fn deserialize_nested_struct() { let mut encoder = Encoder::new(String::new()); let nested = Nested { item: Inner { c: "hello", a: 10, b: "bye" } }; assert_eq!( urlencoded::from_str( &encoder.append_pair("item", r#"{"c":"hello","a":10,"b":"bye"}"#).finish(), ), Ok(nested) ); } #[test] #[ignore] fn deserialize_nested_struct_with_list() { let mut encoder = Encoder::new(String::new()); let nested = Nested { item: InnerList { list: vec![1, 2, 3] } }; assert_eq!( urlencoded::from_str(&encoder.append_pair("item", r#"{"list":[1,2,3]}"#).finish()), Ok(nested) ); } #[test] #[ignore] fn deserialize_nested_list_option() { let mut encoder = Encoder::new(String::new()); let nested = Nested { item: InnerList { list: vec![Some(1), Some(2), None] } }; assert_eq!( urlencoded::from_str(&encoder.append_pair("item", r#"{"list":[1,2,null]}"#).finish()), Ok(nested) ); } ruma-common-0.10.5/tests/serde/url_serialize.rs000064400000000000000000000077251046102023000176370ustar 00000000000000use assert_matches::assert_matches; use form_urlencoded::Serializer as Encoder; use ruma_common::serde::urlencoded::{self, ser::Error}; use serde::Serialize; #[derive(Serialize)] struct NewType(T); #[test] fn serialize_newtype_i32() { let params = &[("field", Some(NewType(11)))]; assert_eq!(urlencoded::to_string(params), Ok("field=11".to_owned())); } #[test] fn serialize_option_map_int() { let params = &[("first", Some(23)), ("middle", None), ("last", Some(42))]; assert_eq!(urlencoded::to_string(params), Ok("first=23&last=42".to_owned())); } #[test] fn serialize_option_map_string() { let params = &[("first", Some("hello")), ("middle", None), ("last", Some("world"))]; assert_eq!(urlencoded::to_string(params), Ok("first=hello&last=world".to_owned())); } #[test] fn serialize_option_map_bool() { let params = &[("one", Some(true)), ("two", Some(false))]; assert_eq!(urlencoded::to_string(params), Ok("one=true&two=false".to_owned())); } #[test] fn serialize_map_bool() { let params = &[("one", true), ("two", false)]; assert_eq!(urlencoded::to_string(params), Ok("one=true&two=false".to_owned())); } #[derive(Serialize)] enum X { A, B, C, } #[test] fn serialize_unit_enum() { let params = &[("one", X::A), ("two", X::B), ("three", X::C)]; assert_eq!(urlencoded::to_string(params), Ok("one=A&two=B&three=C".to_owned())); } #[derive(Serialize)] struct Unit; #[test] fn serialize_unit_struct() { assert_eq!(urlencoded::to_string(Unit), Ok("".to_owned())); } #[test] fn serialize_unit_type() { assert_eq!(urlencoded::to_string(()), Ok("".to_owned())); } #[test] fn serialize_list_of_str() { let params = &[("list", vec!["hello", "world"])]; assert_eq!(urlencoded::to_string(params), Ok("list=hello&list=world".to_owned())); } #[test] fn serialize_multiple_lists() { #[derive(Serialize)] struct Lists { xs: Vec, ys: Vec, } let params = Lists { xs: vec![true, false], ys: vec![3, 2, 1] }; assert_eq!(urlencoded::to_string(params), Ok("xs=true&xs=false&ys=3&ys=2&ys=1".to_owned())); } #[test] fn serialize_nested_list() { let params = &[("list", vec![vec![0_u8]])]; assert_matches!( urlencoded::to_string(params), Err(Error::Custom(s)) if s.contains("unsupported") ); } #[test] fn serialize_list_of_option() { let params = &[("list", vec![Some(10), Some(100)])]; assert_eq!(urlencoded::to_string(params), Ok("list=10&list=100".to_owned())); } #[test] fn serialize_list_of_newtype() { let params = &[("list", vec![NewType("test".to_owned())])]; assert_eq!(urlencoded::to_string(params), Ok("list=test".to_owned())); } #[test] fn serialize_list_of_enum() { let params = &[("item", vec![X::A, X::B, X::C])]; assert_eq!(urlencoded::to_string(params), Ok("item=A&item=B&item=C".to_owned())); } #[test] fn serialize_map() { let mut s = std::collections::BTreeMap::new(); s.insert("hello", "world"); s.insert("seri", "alize"); s.insert("matrix", "ruma"); let encoded = urlencoded::to_string(s).unwrap(); assert_eq!("hello=world&matrix=ruma&seri=alize", encoded); } #[derive(Serialize)] struct Nested { item: T, } #[derive(Serialize)] struct Inner { c: String, a: usize, b: String, } #[derive(Debug, Serialize, PartialEq)] struct InnerList { list: Vec, } #[test] #[ignore] fn serialize_nested_struct() { let mut encoder = Encoder::new(String::new()); let s = Nested { item: Inner { c: "hello".into(), a: 10, b: "bye".into() } }; assert_eq!( encoder.append_pair("item", r#"{"c":"hello","a":10,"b":"bye"}"#).finish(), urlencoded::to_string(s).unwrap() ); } #[test] #[ignore] fn serialize_nested_struct_with_list() { let mut encoder = Encoder::new(String::new()); let s = Nested { item: InnerList { list: vec![1, 2, 3] } }; assert_eq!( encoder.append_pair("item", r#"{"list":[1,2,3]}"#).finish(), urlencoded::to_string(s).unwrap() ); } ruma-common-0.10.5/tests/tests.rs000064400000000000000000000000611046102023000150100ustar 00000000000000mod api; mod events; mod identifiers; mod serde;
\ In reply to \ @user:example.org\
\

This is the first message

\
\ In reply to \ {emote_sign}{sender}\
\ {html_body}\