Add 'ruma-events/' from commit '00692d532e26f58d48ead9589dc823403c6e59a5'

git-subtree-dir: ruma-events
git-subtree-mainline: d59a616e2c363507a89c92f34aa67e86ee2cfb49
git-subtree-split: 00692d532e26f58d48ead9589dc823403c6e59a5
This commit is contained in:
Jonas Platte 2020-06-10 22:07:24 +02:00
commit f304c04d1d
94 changed files with 8685 additions and 0 deletions

View File

@ -0,0 +1,27 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-events
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install beta --profile minimal -c rustfmt -c clippy
rustup default beta
- test: |
cd ruma-events
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --verbose
test_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test_exit ))

View File

@ -0,0 +1,16 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-events
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install 1.40.0 --profile minimal
rustup default 1.40.0
- test: |
cd ruma-events
# Only make sure the code builds with the MSRV. Tests can require later
# Rust versions, don't compile or run them.
cargo build --verbose

View File

@ -0,0 +1,32 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-events
tasks:
- rustup: |
rustup toolchain install nightly --profile minimal
rustup default nightly
# Try installing rustfmt & clippy for nightly, but don't fail the build
# if they are not available
rustup component add rustfmt || true
rustup component add clippy || true
- test: |
cd ruma-events
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
if ( rustup component list | grep -q rustfmt ); then
cargo fmt -- --check
fi
fmt_exit=$?
if ( rustup component list | grep -q clippy ); then
cargo clippy --all-targets --all-features -- -D warnings
fi
clippy_exit=$?
exit $(( $fmt_exit || $clippy_exit ))

View File

@ -0,0 +1,29 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-events
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install stable --profile minimal -c rustfmt -c clippy
rustup default stable
- test: |
cd ruma-events
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --verbose
test_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test_exit ))
# TODO: Add audit task once cargo-audit binary releases are available.
# See https://github.com/RustSec/cargo-audit/issues/66

5
ruma-events/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
Cargo.lock
target
# trybuild generates a `wip` folder when creating or updating a test
wip

312
ruma-events/CHANGELOG.md Normal file
View File

@ -0,0 +1,312 @@
# [unreleased]
Breaking changes:
* Add `alt_aliases` to `CanonicalAliasEventContent`
* Replace `format` and `formatted_body` fields in `TextMessagEventContent`,
`NoticeMessageEventContent` and `EmoteMessageEventContent` with `formatted: FormattedBody`
* Rename `override_rules` in `push_rules::Ruleset` to `override_`
* Change `push_rules::PushCondition` variants from newtype variants with separate inner types to
struct variants
* This change removes the types `EventMatchCondition`, `RoomMemberCountCondition` and
`SenderNotificationPermissionCondition`
Improvements:
* Add `room::MessageFormat` and `room::FormattedBody`
# 0.21.3
Bug fixes:
* Fix `m.room.message` event serialization
Improvements:
* Skip serialization of `federate` field in `room::create::CreateEventContent`
if it is `true` (the default value)
* `room::power_levels::PowerLevelsEventContent` now implements `Default`
# 0.21.2
Improvements:
* Update dependencies
# 0.21.1
Improvements:
* Add `EventJson::into_json`
# 0.21.0
Breaking changes:
* Replace `EventResult` with a new construct, `EventJson`
* Instead of only capturing the json value if deserialization failed, we now
now always capture it. To improve deserialization performance at the same
time, we no longer use `serde_json::Value` internally and instead
deserialize events as `Box<serde_json::value::RawValue>`. `EventJson` is
simply a wrapper around that owned value type that additionally holds a
generic argument: the type as which clients will usually want to deserialize
the raw value.
* Add `struct UnsignedData` and update all `unsigned` fields types from
`BTreeMap<String, Value>` to this new type.
* To access any additional fields of the `unsigned` property of an event,
deserialize the `EventJson` to another type that captures the field(s) you
are interested in.
* Add fields `format` and `formatted_body` to `room::message::NoticeMessageEventContent`
* Remove `room::message::MessageType`
* Remove useless `algorithm` fields from encrypted event content structs
* Remove `PartialEq` implementations for most types
* Since we're now using `serde_json::value::RawValue`, deriving no longer works
* Update the representation of `push_rules::Tweak`
* Raise minimum supported Rust version to 1.40.0
# 0.20.0
Improvements:
* Update ruma-identifiers to 0.16.0
# 0.19.0
Breaking changes:
* Update ruma-identifiers to 0.15.1
* Change timestamps, including `origin_server_rs` from `UInt` to `SystemTime`
* Change all usages of `HashMap` to `BTreeMap`
* To support this, `EventType` now implements `PartialOrd` and `Ord`
# 0.18.0
Breaking changes:
* Update unsigned field's type from `Option<Value>` to `Map<String, Value>`
Improvements:
* Add a convenience constructor to create a plain-text `TextMessageEventContent`
* Add `m.dummy` events to the to-device event collection
# 0.17.0
Breaking changes:
* `collections::only` no longer exports a `raw` submodule. It was never meant ot be exported in the first place.
* Renamed `stripped::{StrippedState => AnyStrippedStateEvent, StrippedStateContent => StrippedStateEvent}`
Improvements:
* Added `to_device` module with to-device variants of events (as found in the `to_device` section of a sync response)
* Added a helper method for computing the membership change from a `MemberEvent`
Bug fixes:
* Fixed missing `m.` in `m.relates_to` field of room messages
* Fixed (de)serialization of encrypted events using `m.olm.v1.curve25519-aes-sha2`
# 0.16.0
Breaking changes:
* `TryFromRaw::try_from_raw`'s signature has been simplified. The previous signature was a relict that was no longer sensible.
* All remaining non-optional `room_id` event fields (not event content fields) have been made optional
Improvements:
* `NameEvent`s are now validated properly and will be rejected if the `name` field is longer than 255 bytes.
# 0.15.1
Bug fixes:
* Deserialization of custom events as part of the types from `ruma_events::collections::{all, only}` was implemented (this was missing after the big fallible deserializion rewrite in 0.15.0)
# 0.15.0
Improvements:
* `ruma-events` now exports a new type, `EventResult`
* For any event or event content type `T` inside a larger type that should support deserialization you can use `EventResult<T>` instead
* Conceptually, it is the same as `Result<T, InvalidEvent>`
* `InvalidEvent` can represent either a deserialization error (the event's structure did not match) or a validation error (some additional constraints defined in the matrix spec were violated)
* It also contains the original value that was attempted to be deserialized into `T` in `serde_json::Value` form
Breaking changes:
* The `FromStr` implementations for event types were removed (they were the previous implementation of fallible deserialization, but were never integrated in ruma-client-api because they didn't interoperate well with serde derives)
# 0.14.0
Breaking changes:
* Updated to ruma-identifiers 0.14.0.
Improvements:
* ruma-events is now checked against the RustSec advisory database.
# 0.13.0
Breaking changes:
* Events and their content types no longer implement `Deserialize` and instead implement `FromStr` and `TryFrom<&str>`, which take a `&str` of JSON data and return a new `InvalidEvent` type on error.
* Integers are now represented using the `Int` and `UInt` types from the `js_int` crate to ensure they are within the JavaScript-interoperable range mandated by the Matrix specification.
* Some event types have new fields or new default values for previous fields to bring them up to date with version r0.5.0 of the client-server specification.
* Some event types no longer have public fields and instead use a constructor function to perform validations not represented by the type system.
* All enums now include a "nonexhaustive" variant to prevent exhaustive pattern matching. This will change to use the `#[nonexhaustive]` attribute when it is stabilized.
* `ParseError` has been renamed `FromStrError`.
New features:
* This release brings ruma-events completely up to date with version r0.5.0 of the client-server specification. All previously supported events have been updated as necessary and the following events have newly added support:
* m.dummy
* m.forwarded_room_key
* m.fully_read
* m.ignored_user_list
* m.key.verification.accept
* m.key.verification.cancel
* m.key.verification.key
* m.key.verification.mac
* m.key.verification.request
* m.key.verification.start
* m.push_rules
* m.key.encrypted
* m.key.encryption
* m.key.server_acl
* m.key.tombstone
* m.room_key
* m.room_key_request
* m.sticker
Improvements:
* Improved documentation for the crate and for many types.
* Added many new tests.
* rustfmt and clippy are now used to ensure consistent formatting and improved code quality.
# 0.12.0
Improvements:
* ruma-events now runs on stable Rust, requiring version 1.34 or higher.
Bug fixes:
* `CanonicalAliasEvent` and `NameEvent` now allow content being absent, null, or empty, as per the spec.
# 0.11.1
Breaking changes:
* `RoomId` is now optional in certain places where it may be absent, notably the responses of the `/sync` API endpoint.
* A `sender` field has been added to the `StrippedStateContent` type.
Improvements:
* Depend on serde's derive feature rather than serde_derive directly for simplified imports.
* Update to Rust 2018 idioms.
# 0.11.0
Breaking changes:
* The presence event has been modified to match the latest version of the spec. The spec was corrected to match the behavior of the Synapse homeserver.
Improvements:
* Dependencies have been updated to the latest versions.
# 0.10.0
Breaking changes:
* The `EventType`, and collections enums have new variants to support new events.
* The `extra_content` method has been removed from the Event trait.
* The `user_id` method from the `RoomEvent` trait has been renamed `sender` to match the specification.
* The `origin_server_ts` value is now required for room events and is supported via a new `origin_server_ts` method on the `RoomEvent` trait.
* `MemberEventContent` has a new `is_direct` field.
* `FileMessageEventContent` has a new `filename` field.
* File and thumbnail info have been moved from several message types to dedicated `FileInfo`, `ImageInfo`, and `ThumbnailInfo` types.
* `LocationMessageEventContent` has a new info field.
* `PresenceEventContent`'s `currently_active` field has changed from `bool` to `Option`.
* `TypingEventContent` contains a vector of `UserId`s instead of `EventId`s.
* Height and width fields named `h` and `w` in the spec now use the full names `height` and `width` for their struct field names, but continue to serialize to the single-letter names.
New features:
* ruma-events now supports all events according to r0.3.0 of the Matrix client-server specification.
* Added new event: `m.room.pinned_events`.
* Added new event: `m.direct`.
Bug fixes:
* Several places where struct fields used the wrong key when serialized to JSON have been corrected.
* Fixed grammar issues in documentation.
# 0.9.0
Improvements:
* Added default values for various power level attributes.
* Removed Serde trait bounds on `StrippedStateContent`'s generic parameter.
* Updated to version 0.4 of ruma-signatures.
# 0.8.0
Breaking changes
* Updated serde to the 1.0 series.
# 0.7.0
Bug fixes:
* Make the `federate` field optional when creating a room.
# 0.6.0
Breaking changes:
* Updated ruma-identifiers to the 0.9 series.
# 0.5.0
Breaking changes:
* Updated ruma-identifiers to the 0.8 series.
# 0.4.1
Improvements:
* Relaxed version constraints on dependent crates to allow updating to new patch level versions.
# 0.4.0
Breaking changes:
* Updated serde to the 0.9 series.
The public API remains the same.
# 0.3.0
Improvements:
* `ruma_events::presence::PresenceState` now implements `Display` and `FromStr`.
# 0.2.0
Improvements:
* Added missing "stripped" versions of some state events.
* All "stripped" versions of state events are now serializable.
# 0.1.0
Initial release.

34
ruma-events/Cargo.toml Normal file
View File

@ -0,0 +1,34 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
categories = ["api-bindings"]
description = "Serializable types for the events in the Matrix specification."
documentation = "https://docs.rs/ruma-events"
homepage = "https://github.com/ruma/ruma-events"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-events"
readme = "README.md"
repository = "https://github.com/ruma/ruma-events"
version = "0.21.3"
edition = "2018"
[dependencies]
js_int = { version = "0.1.5", features = ["serde"] }
ruma-common = "0.1.3"
ruma-events-macros = { path = "ruma-events-macros", version = "=0.21.3" }
ruma-identifiers = "0.16.2"
ruma-serde = "0.2.2"
serde = { version = "1.0.111", features = ["derive"] }
serde_json = { version = "1.0.53", features = ["raw_value"] }
strum = { version = "0.18.0", features = ["derive"] }
[dev-dependencies]
maplit = "1.0.2"
matches = "0.1.8"
ruma-identifiers = { version = "0.16.2", features = ["rand"] }
trybuild = "1.0.28"
[workspace]
members = [
"ruma-events-macros",
]

20
ruma-events/LICENSE Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2015 Jimmy Cuadra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

15
ruma-events/README.md Normal file
View File

@ -0,0 +1,15 @@
# ruma-events
[![crates.io page](https://img.shields.io/crates/v/ruma-events.svg)](https://crates.io/crates/ruma-events)
[![docs.rs page](https://docs.rs/ruma-events/badge.svg)](https://docs.rs/ruma-events/)
![license: MIT](https://img.shields.io/crates/l/ruma-events.svg)
**ruma-events** contains serializable types for the events in the [Matrix](https://matrix.org/) specification that can be shared by client and server code.
## Minimum Rust version
ruma-events requires Rust 1.40.0 or later.
## Documentation
ruma-events has [comprehensive documentation](https://docs.rs/ruma-events) available on docs.rs.

View File

@ -0,0 +1,20 @@
# [unreleased]
# 0.3.0
Breaking changes:
* Update `event_type` in `ruma_events!` to refer to the serialized form of the
event type, not the variant of `ruma_events::EventType`
Improvements:
* Split `FromRaw` implementation generation from `ruma_event!` into a separate
proc-macro
# 0.2.0
Improvements:
* Code generation was updated to account for the changes in ruma-events 0.15
* Dependencies were updated (notably to syn 1.0 and quote 1.0)

View File

@ -0,0 +1,27 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
categories = ["api-bindings", "web-programming"]
description = "A procedural macro used by the ruma-events crate."
documentation = "https://docs.rs/ruma-events-macros"
edition = "2018"
homepage = "https://github.com/ruma/ruma-events-macros"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-events-macros"
readme = "README.md"
repository = "https://github.com/ruma/ruma-api-macros"
version = "0.21.3"
[dependencies]
syn = { version = "1.0.25", features = ["full"] }
quote = "1.0.6"
proc-macro2 = "1.0.17"
[lib]
proc-macro = true
[dev-dependencies]
ruma-identifiers = "0.16.1"
serde_json = "1.0.53"
js_int = { version = "0.1.5", features = ["serde"] }
serde = { version = "1.0.110", features = ["derive"] }

View File

@ -0,0 +1,13 @@
# ruma-events-macros
[![Build Status](https://travis-ci.org/ruma/ruma-events-macros.svg?branch=master)](https://travis-ci.org/ruma/ruma-events-macros)
**ruma-events-macros** provides a procedural macro for easily generating event types for [ruma-events](https://github.com/ruma/ruma-events).
## Documentation
ruma-events-macros has [comprehensive documentation](https://docs.rs/ruma-events-macros) available on docs.rs.
## License
[MIT](http://opensource.org/licenses/MIT)

View File

@ -0,0 +1,192 @@
//! Implementation of the content_enum type macro.
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
parse::{self, Parse, ParseStream},
Attribute, Expr, ExprLit, Ident, Lit, LitStr, Token,
};
/// Create a content enum from `ContentEnumInput`.
pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream> {
let attrs = &input.attrs;
let ident = &input.name;
let event_type_str = &input.events;
let variants = input.events.iter().map(to_camel_case).collect::<Vec<_>>();
let content = input
.events
.iter()
.map(to_event_content_path)
.collect::<Vec<_>>();
let content_enum = quote! {
#( #attrs )*
#[derive(Clone, Debug, ::serde::Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum #ident {
#(
#[doc = #event_type_str]
#variants(#content)
),*
}
};
let event_content_impl = quote! {
impl ::ruma_events::EventContent for #ident {
fn event_type(&self) -> &str {
match self {
#( Self::#variants(content) => content.event_type() ),*
}
}
fn from_parts(event_type: &str, input: Box<::serde_json::value::RawValue>) -> Result<Self, String> {
match event_type {
#(
#event_type_str => {
let content = #content::from_parts(event_type, input)?;
Ok(#ident::#variants(content))
},
)*
ev => Err(format!("event not supported {}", ev)),
}
}
}
};
let marker_trait_impls = marker_traits(ident);
Ok(quote! {
#content_enum
#event_content_impl
#marker_trait_impls
})
}
fn marker_traits(ident: &Ident) -> TokenStream {
match ident.to_string().as_str() {
"AnyStateEventContent" => quote! {
impl ::ruma_events::RoomEventContent for #ident {}
impl ::ruma_events::StateEventContent for #ident {}
},
"AnyMessageEventContent" => quote! {
impl ::ruma_events::RoomEventContent for #ident {}
impl ::ruma_events::MessageEventContent for #ident {}
},
"AnyEphemeralRoomEventContent" => quote! {
impl ::ruma_events::EphemeralRoomEventContent for #ident {}
},
"AnyBasicEventContent" => quote! {
impl ::ruma_events::BasicEventContent for #ident {}
},
_ => TokenStream::new(),
}
}
fn to_event_content_path(
name: &LitStr,
) -> syn::punctuated::Punctuated<syn::Token![::], syn::PathSegment> {
let span = name.span();
let name = name.value();
assert_eq!(&name[..2], "m.");
let path = name[2..].split('.').collect::<Vec<_>>();
let event_str = path.last().unwrap();
let event = event_str
.split('_')
.map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
.collect::<String>();
let content_str = Ident::new(&format!("{}EventContent", event), span);
let path = path.iter().map(|s| Ident::new(s, span));
syn::parse_quote! {
::ruma_events::#( #path )::*::#content_str
}
}
/// Splits the given `event_type` string on `.` and `_` removing the `m.room.` then
/// camel casing to give the `EventContent` struct name.
pub(crate) fn to_camel_case(name: &LitStr) -> Ident {
let span = name.span();
let name = name.value();
if &name[..2] != "m." {
panic!(
"well-known matrix events have to start with `m.` found `{}`",
name,
)
}
let s = name[2..]
.split(&['.', '_'] as &[char])
.map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
.collect::<String>();
Ident::new(&s, span)
}
/// Custom keywords for the `event_content_content_enum!` macro
mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(events);
}
/// The entire `event_content_content_enum!` macro structure directly as it appears in the source code..
pub struct ContentEnumInput {
/// Outer attributes on the field, such as a docstring.
pub attrs: Vec<Attribute>,
/// The name of the event.
pub name: Ident,
/// An array of valid matrix event types. This will generate the variants of the event content type "name".
/// There needs to be a corresponding variant in `ruma_events::EventType` for
/// this event (converted to a valid Rust-style type name by stripping `m.`, replacing the
/// remaining dots by underscores and then converting from snake_case to CamelCase).
pub events: Vec<LitStr>,
}
impl Parse for ContentEnumInput {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
// name field
input.parse::<kw::name>()?;
input.parse::<Token![:]>()?;
// the name of our content_enum enum
let name: Ident = input.parse()?;
input.parse::<Token![,]>()?;
// events field
input.parse::<kw::events>()?;
input.parse::<Token![:]>()?;
// an array of event names `["m.room.whatever"]`
let ev_array = input.parse::<syn::ExprArray>()?;
let events = ev_array
.elems
.into_iter()
.map(|item| {
if let Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
}) = item
{
Ok(lit_str)
} else {
let msg = "values of field `events` are required to be a string literal";
Err(syn::Error::new_spanned(item, msg))
}
})
.collect::<syn::Result<_>>()?;
Ok(Self {
attrs,
name,
events,
})
}
}

View File

@ -0,0 +1,287 @@
//! Implementation of the top level `*Event` derive macro.
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Ident};
/// Derive `Event` macro code generation.
pub fn expand_event(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = &input.ident;
let (impl_gen, ty_gen, where_clause) = input.generics.split_for_impl();
let is_generic = !input.generics.params.is_empty();
let fields = if let Data::Struct(DataStruct { fields, .. }) = input.data.clone() {
if let Fields::Named(FieldsNamed { named, .. }) = fields {
if !named.iter().any(|f| f.ident.as_ref().unwrap() == "content") {
return Err(syn::Error::new(
Span::call_site(),
"struct must contain a `content` field",
));
}
named.into_iter().collect::<Vec<_>>()
} else {
return Err(syn::Error::new_spanned(
fields,
"the `Event` derive only supports named fields",
));
}
} else {
return Err(syn::Error::new_spanned(
input.ident,
"the `Event` derive only supports structs with named fields",
));
};
let serialize_fields = fields
.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
if name == "prev_content" {
quote! {
if let Some(content) = self.prev_content.as_ref() {
state.serialize_field("prev_content", content)?;
}
}
} else if name == "origin_server_ts" {
quote! {
let time_since_epoch =
self.origin_server_ts.duration_since(::std::time::UNIX_EPOCH).unwrap();
let timestamp = <::js_int::UInt as ::std::convert::TryFrom<_>>::try_from(time_since_epoch.as_millis())
.map_err(S::Error::custom)?;
state.serialize_field("origin_server_ts", &timestamp)?;
}
} else if name == "unsigned" {
quote! {
if !self.unsigned.is_empty() {
state.serialize_field("unsigned", &self.unsigned)?;
}
}
} else {
quote! {
state.serialize_field(stringify!(#name), &self.#name)?;
}
}
})
.collect::<Vec<_>>();
let serialize_impl = quote! {
impl #impl_gen ::serde::ser::Serialize for #ident #ty_gen #where_clause {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ::serde::ser::Serializer,
{
use ::serde::ser::{SerializeStruct as _, Error as _};
let event_type = ::ruma_events::EventContent::event_type(&self.content);
let mut state = serializer.serialize_struct(stringify!(#ident), 7)?;
state.serialize_field("type", event_type)?;
#( #serialize_fields )*
state.end()
}
}
};
let deserialize_impl = expand_deserialize_event(is_generic, input, fields)?;
Ok(quote! {
#serialize_impl
#deserialize_impl
})
}
fn expand_deserialize_event(
is_generic: bool,
input: DeriveInput,
fields: Vec<Field>,
) -> syn::Result<TokenStream> {
let ident = &input.ident;
// we know there is a content field already
let content_type = fields
.iter()
// we also know that the fields are named and have an ident
.find(|f| f.ident.as_ref().unwrap() == "content")
.map(|f| f.ty.clone())
.unwrap();
let (impl_generics, ty_gen, where_clause) = input.generics.split_for_impl();
let enum_variants = fields
.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
to_camel_case(name)
})
.collect::<Vec<_>>();
let deserialize_var_types = fields
.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
let ty = &field.ty;
if name == "content" || name == "prev_content" {
if is_generic {
quote! { Box<::serde_json::value::RawValue> }
} else {
quote! { #content_type }
}
} else if name == "origin_server_ts" {
quote! { ::js_int::UInt }
} else {
quote! { #ty }
}
})
.collect::<Vec<_>>();
let ok_or_else_fields = fields
.iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
if name == "content" {
if is_generic {
quote! {
let json = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?;
let content = C::from_parts(&event_type, json).map_err(A::Error::custom)?;
}
} else {
quote! {
let content = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?;
}
}
} else if name == "prev_content" {
if is_generic {
quote! {
let prev_content = if let Some(json) = prev_content {
Some(C::from_parts(&event_type, json).map_err(A::Error::custom)?)
} else {
None
};
}
} else {
quote! {
let prev_content = if let Some(content) = prev_content {
Some(content)
} else {
None
};
}
}
} else if name == "origin_server_ts" {
quote! {
let origin_server_ts = origin_server_ts
.map(|time| {
let t = time.into();
::std::time::UNIX_EPOCH + ::std::time::Duration::from_millis(t)
})
.ok_or_else(|| ::serde::de::Error::missing_field("origin_server_ts"))?;
}
} else if name == "unsigned" {
quote! { let unsigned = unsigned.unwrap_or_default(); }
} else {
quote! {
let #name = #name.ok_or_else(|| {
::serde::de::Error::missing_field(stringify!(#name))
})?;
}
}
})
.collect::<Vec<_>>();
let field_names = fields.iter().flat_map(|f| &f.ident).collect::<Vec<_>>();
let deserialize_impl_gen = if is_generic {
let gen = &input.generics.params;
quote! { <'de, #gen> }
} else {
quote! { <'de> }
};
let deserialize_phantom_type = if is_generic {
quote! { ::std::marker::PhantomData }
} else {
quote! {}
};
Ok(quote! {
impl #deserialize_impl_gen ::serde::de::Deserialize<'de> for #ident #ty_gen #where_clause {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
// since this is represented as an enum we have to add it so the JSON picks it up
Type,
#( #enum_variants ),*
}
/// Visits the fields of an event struct to handle deserialization of
/// the `content` and `prev_content` fields.
struct EventVisitor #impl_generics (#deserialize_phantom_type #ty_gen);
impl #deserialize_impl_gen ::serde::de::Visitor<'de> for EventVisitor #ty_gen #where_clause {
type Value = #ident #ty_gen;
fn expecting(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(formatter, "struct implementing {}", stringify!(#content_type))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: ::serde::de::MapAccess<'de>,
{
use ::serde::de::Error as _;
let mut event_type: Option<String> = None;
#( let mut #field_names: Option<#deserialize_var_types> = None; )*
while let Some(key) = map.next_key()? {
match key {
Field::Type => {
if event_type.is_some() {
return Err(::serde::de::Error::duplicate_field("type"));
}
event_type = Some(map.next_value()?);
}
#(
Field::#enum_variants => {
if #field_names.is_some() {
return Err(::serde::de::Error::duplicate_field(stringify!(#field_names)));
}
#field_names = Some(map.next_value()?);
}
)*
}
}
let event_type = event_type.ok_or_else(|| ::serde::de::Error::missing_field("type"))?;
#( #ok_or_else_fields )*
Ok(#ident {
#( #field_names ),*
})
}
}
deserializer.deserialize_map(EventVisitor(#deserialize_phantom_type))
}
}
})
}
/// CamelCase's a field ident like "foo_bar" to "FooBar".
fn to_camel_case(name: &Ident) -> Ident {
let span = name.span();
let name = name.to_string();
let s = name
.split('_')
.map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
.collect::<String>();
Ident::new(&s, span)
}

View File

@ -0,0 +1,126 @@
//! Implementations of the MessageEventContent and StateEventContent derive macro.
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
DeriveInput, LitStr, Token,
};
/// Parses attributes for `*EventContent` derives.
///
/// `#[ruma_event(type = "m.room.alias")]`
enum EventMeta {
/// Variant holds the "m.whatever" event type.
Type(LitStr),
}
impl Parse for EventMeta {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<Token![type]>()?;
input.parse::<Token![=]>()?;
Ok(EventMeta::Type(input.parse::<LitStr>()?))
}
}
/// Create an `EventContent` implementation for a struct.
pub fn expand_event_content(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = &input.ident;
let event_type_attr = input
.attrs
.iter()
.find(|attr| attr.path.is_ident("ruma_event"))
.ok_or_else(|| {
let msg = "no event type attribute found, \
add `#[ruma_event(type = \"any.room.event\")]` \
below the event content derive";
syn::Error::new(Span::call_site(), msg)
})?;
let event_type = {
let event_meta = event_type_attr.parse_args::<EventMeta>()?;
let EventMeta::Type(lit) = event_meta;
lit
};
Ok(quote! {
impl ::ruma_events::EventContent for #ident {
fn event_type(&self) -> &str {
#event_type
}
fn from_parts(
ev_type: &str,
content: Box<::serde_json::value::RawValue>
) -> Result<Self, String> {
if ev_type != #event_type {
return Err(format!("expected `{}` found {}", #event_type, ev_type));
}
::serde_json::from_str(content.get()).map_err(|e| e.to_string())
}
}
})
}
/// Create a `BasicEventContent` implementation for a struct
pub fn expand_basic_event_content(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = input.ident.clone();
let event_content_impl = expand_event_content(input)?;
Ok(quote! {
#event_content_impl
impl ::ruma_events::BasicEventContent for #ident { }
})
}
/// Create a `EphemeralRoomEventContent` implementation for a struct
pub fn expand_ephemeral_room_event_content(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = input.ident.clone();
let event_content_impl = expand_event_content(input)?;
Ok(quote! {
#event_content_impl
impl ::ruma_events::EphemeralRoomEventContent for #ident { }
})
}
/// Create a `RoomEventContent` implementation for a struct.
pub fn expand_room_event_content(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = input.ident.clone();
let event_content_impl = expand_event_content(input)?;
Ok(quote! {
#event_content_impl
impl ::ruma_events::RoomEventContent for #ident { }
})
}
/// Create a `MessageEventContent` implementation for a struct
pub fn expand_message_event_content(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = input.ident.clone();
let room_ev_content = expand_room_event_content(input)?;
Ok(quote! {
#room_ev_content
impl ::ruma_events::MessageEventContent for #ident { }
})
}
/// Create a `StateEventContent` implementation for a struct
pub fn expand_state_event_content(input: DeriveInput) -> syn::Result<TokenStream> {
let ident = input.ident.clone();
let room_ev_content = expand_room_event_content(input)?;
Ok(quote! {
#room_ev_content
impl ::ruma_events::StateEventContent for #ident { }
})
}

View File

@ -0,0 +1,102 @@
//! Crate `ruma_events_macros` provides a procedural macro for generating
//! [ruma-events](https://github.com/ruma/ruma-events) events.
//!
//! See the documentation for the individual macros for usage details.
#![deny(
missing_copy_implementations,
missing_debug_implementations,
missing_docs
)]
extern crate proc_macro;
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use self::{
content_enum::{expand_content_enum, ContentEnumInput},
event::expand_event,
event_content::{
expand_basic_event_content, expand_ephemeral_room_event_content, expand_event_content,
expand_message_event_content, expand_room_event_content, expand_state_event_content,
},
};
mod content_enum;
mod event;
mod event_content;
/// Generates a content enum to represent the various Matrix event types.
///
/// This macro also implements the necessary traits for the type to serialize and deserialize
/// itself.
#[proc_macro]
pub fn event_content_enum(input: TokenStream) -> TokenStream {
let content_enum_input = syn::parse_macro_input!(input as ContentEnumInput);
expand_content_enum(content_enum_input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates an implementation of `ruma_events::EventContent`.
#[proc_macro_derive(EventContent, attributes(ruma_event))]
pub fn derive_event_content(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_event_content(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates an implementation of `ruma_events::BasicEventContent` and it's super traits.
#[proc_macro_derive(BasicEventContent, attributes(ruma_event))]
pub fn derive_basic_event_content(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_basic_event_content(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates an implementation of `ruma_events::RoomEventContent` and it's super traits.
#[proc_macro_derive(RoomEventContent, attributes(ruma_event))]
pub fn derive_room_event_content(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_room_event_content(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates an implementation of `ruma_events::MessageEventContent` and it's super traits.
#[proc_macro_derive(MessageEventContent, attributes(ruma_event))]
pub fn derive_message_event_content(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_message_event_content(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates an implementation of `ruma_events::StateEventContent` and it's super traits.
#[proc_macro_derive(StateEventContent, attributes(ruma_event))]
pub fn derive_state_event_content(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_state_event_content(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates an implementation of `ruma_events::EphemeralRoomEventContent` and it's super traits.
#[proc_macro_derive(EphemeralRoomEventContent, attributes(ruma_event))]
pub fn derive_ephemeral_room_event_content(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_ephemeral_room_event_content(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Generates implementations needed to serialize and deserialize Matrix events.
#[proc_macro_derive(Event, attributes(ruma_event))]
pub fn derive_state_event(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_event(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}

View File

@ -0,0 +1,70 @@
use std::fmt::{Display, Formatter, Result as FmtResult};
use serde::{Deserialize, Serialize};
/// An encryption algorithm to be used to encrypt messages sent to a room.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(from = "String", into = "String")]
pub enum Algorithm {
/// Olm version 1 using Curve25519, AES-256, and SHA-256.
OlmV1Curve25519AesSha2,
/// Megolm version 1 using AES-256 and SHA-256.
MegolmV1AesSha2,
/// Any algorithm that is not part of the specification.
Custom(String),
}
impl Display for Algorithm {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let algorithm_str = match *self {
Algorithm::OlmV1Curve25519AesSha2 => "m.olm.v1.curve25519-aes-sha2",
Algorithm::MegolmV1AesSha2 => "m.megolm.v1.aes-sha2",
Algorithm::Custom(ref algorithm) => algorithm,
};
write!(f, "{}", algorithm_str)
}
}
impl<T> From<T> for Algorithm
where
T: Into<String> + AsRef<str>,
{
fn from(s: T) -> Algorithm {
match s.as_ref() {
"m.olm.v1.curve25519-aes-sha2" => Algorithm::OlmV1Curve25519AesSha2,
"m.megolm.v1.aes-sha2" => Algorithm::MegolmV1AesSha2,
_ => Algorithm::Custom(s.into()),
}
}
}
impl From<Algorithm> for String {
fn from(algorithm: Algorithm) -> String {
algorithm.to_string()
}
}
#[cfg(test)]
mod tests {
use ruma_serde::test::serde_json_eq;
use serde_json::json;
use super::*;
#[test]
fn serialize_and_deserialize_from_display_form() {
serde_json_eq(Algorithm::MegolmV1AesSha2, json!("m.megolm.v1.aes-sha2"));
serde_json_eq(
Algorithm::OlmV1Curve25519AesSha2,
json!("m.olm.v1.curve25519-aes-sha2"),
);
serde_json_eq(
Algorithm::Custom("io.ruma.test".to_string()),
json!("io.ruma.test"),
);
}
}

35
ruma-events/src/call.rs Normal file
View File

@ -0,0 +1,35 @@
//! Modules for events in the *m.call* namespace.
//!
//! This module also contains types shared by events in its child namespaces.
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
pub mod answer;
pub mod candidates;
pub mod hangup;
pub mod invite;
/// A VoIP session description.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct SessionDescription {
/// The type of session description.
#[serde(rename = "type")]
pub session_type: SessionDescriptionType,
/// The SDP text of the session description.
pub sdp: String,
}
/// The type of VoIP session description.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SessionDescriptionType {
/// An answer.
Answer,
/// An offer.
Offer,
}

View File

@ -0,0 +1,25 @@
//! Types for the *m.call.answer* event.
use js_int::UInt;
use ruma_events_macros::MessageEventContent;
use serde::{Deserialize, Serialize};
use super::SessionDescription;
use crate::MessageEvent;
/// This event is sent by the callee when they wish to answer the call.
pub type AnswerEvent = MessageEvent<AnswerEventContent>;
/// The payload for `AnswerEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.call.answer")]
pub struct AnswerEventContent {
/// The VoIP session description object. The session description type must be *answer*.
pub answer: SessionDescription,
/// The ID of the call this event relates to.
pub call_id: String,
/// The version of the VoIP specification this messages adheres to.
pub version: UInt,
}

View File

@ -0,0 +1,39 @@
//! Types for the *m.call.candidates* event.
use js_int::UInt;
use ruma_events_macros::MessageEventContent;
use serde::{Deserialize, Serialize};
use crate::MessageEvent;
/// 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.
pub type CandidatesEvent = MessageEvent<CandidatesEventContent>;
/// The payload for `CandidatesEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.call.candidates")]
pub struct CandidatesEventContent {
/// The ID of the call this event relates to.
pub call_id: String,
/// A list of candidates.
pub candidates: Vec<Candidate>,
/// The version of the VoIP specification this messages adheres to.
pub version: UInt,
}
/// An ICE (Interactive Connectivity Establishment) candidate.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[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,
}

View File

@ -0,0 +1,43 @@
//! Types for the *m.call.hangup* event.
use js_int::UInt;
use ruma_events_macros::MessageEventContent;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::MessageEvent;
/// Sent by either party to signal their termination of the call. This can be sent either once the
/// call has has been established or before to abort the call.
pub type HangupEvent = MessageEvent<HangupEventContent>;
/// The payload for `HangupEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.call.hangup")]
pub struct HangupEventContent {
/// The ID of the call this event relates to.
pub call_id: String,
/// The version of the VoIP specification this messages adheres to.
pub version: UInt,
/// Optional error reason for the hangup.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<Reason>,
}
/// A reason for a hangup.
///
/// This 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.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Reason {
/// ICE negotiation failure.
IceFailed,
/// Party did not answer in time.
InviteTimeout,
}

View File

@ -0,0 +1,30 @@
//! Types for the *m.call.invite* event.
use js_int::UInt;
use ruma_events_macros::MessageEventContent;
use serde::{Deserialize, Serialize};
use super::SessionDescription;
use crate::MessageEvent;
/// This event is sent by the caller when they wish to establish a call.
pub type InviteEvent = MessageEvent<InviteEventContent>;
/// The payload for `InviteEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.call.invite")]
pub struct InviteEventContent {
/// A unique identifer for the call.
pub call_id: String,
/// 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. The session description type must be *offer*.
pub offer: SessionDescription,
/// The version of the VoIP specification this messages adheres to.
pub version: UInt,
}

76
ruma-events/src/custom.rs Normal file
View File

@ -0,0 +1,76 @@
//! Types for custom events outside of the Matrix specification.
use std::time::SystemTime;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde::Serialize;
use serde_json::Value as JsonValue;
use crate::UnsignedData;
// TODO: (De)serialization
/// A custom event's type and `content` JSON object.
#[derive(Clone, Debug, Serialize)]
pub struct CustomEventContent {
/// The event type string.
#[serde(skip)]
pub event_type: String,
/// The actual `content` JSON object.
pub json: JsonValue,
}
/// A custom event not covered by the Matrix specification.
#[derive(Clone, Debug)]
pub struct CustomBasicEvent {
/// The event's content.
pub content: CustomEventContent,
}
/// A custom message event not covered by the Matrix specification.
#[derive(Clone, Debug)]
pub struct CustomMessageEvent {
/// The event's content.
pub content: CustomEventContent,
/// Time on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// The unique identifier for the room associated with this event.
pub room_id: Option<RoomId>,
/// The unique identifier for the user who sent this event.
pub sender: UserId,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// A custom state event not covered by the Matrix specification.
#[derive(Clone, Debug)]
pub struct CustomStateEvent {
/// The event's content.
pub content: CustomEventContent,
/// The unique identifier for the event.
pub event_id: EventId,
/// Time on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// The previous content for this state key, if any.
pub prev_content: Option<CustomEventContent>,
/// The unique identifier for the room associated with this event.
pub room_id: Option<RoomId>,
/// The unique identifier for the user who sent this event.
pub sender: UserId,
/// A key that determines which piece of room state the event represents.
pub state_key: String,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}

90
ruma-events/src/direct.rs Normal file
View File

@ -0,0 +1,90 @@
//! Types for the *m.direct* event.
use std::{
collections::BTreeMap,
ops::{Deref, DerefMut},
};
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::{RoomId, UserId};
use serde::{Deserialize, Serialize};
/// Informs the client about the rooms that are considered direct by a user.
pub type DirectEvent = crate::BasicEvent<DirectEventContent>;
/// The payload for `DirectEvent`.
///
/// A mapping of `UserId`s to a list of `RoomId`s which are considered *direct* for that
/// particular user.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.direct")]
pub struct DirectEventContent(pub BTreeMap<UserId, Vec<RoomId>>);
impl Deref for DirectEventContent {
type Target = BTreeMap<UserId, Vec<RoomId>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DirectEventContent {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use ruma_identifiers::{RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{DirectEvent, DirectEventContent};
use crate::EventJson;
#[test]
fn serialization() {
let mut content = DirectEventContent(BTreeMap::new());
let alice = UserId::new("ruma.io").unwrap();
let room = vec![RoomId::new("ruma.io").unwrap()];
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 alice = UserId::new("ruma.io").unwrap();
let rooms = vec![
RoomId::new("ruma.io").unwrap(),
RoomId::new("ruma.io").unwrap(),
];
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::<EventJson<_>>(json_data)
.unwrap()
.deserialize()
.unwrap();
let direct_rooms = event.content.get(&alice).unwrap();
assert!(direct_rooms.contains(&rooms[0]));
assert!(direct_rooms.contains(&rooms[1]));
}
}

75
ruma-events/src/dummy.rs Normal file
View File

@ -0,0 +1,75 @@
//! Types for the *m.dummy* event.
use std::ops::{Deref, DerefMut};
use ruma_events_macros::BasicEventContent;
use ruma_serde::empty::Empty;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// This event type 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.
pub type DummyEvent = BasicEvent<DummyEventContent>;
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.dummy")]
/// The payload for `DummyEvent`.
pub struct DummyEventContent(pub Empty);
impl Deref for DummyEventContent {
type Target = Empty;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DummyEventContent {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)]
mod tests {
use super::{DummyEvent, DummyEventContent, Empty};
use crate::EventJson;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test]
fn serialization() {
let dummy_event = DummyEvent {
content: DummyEventContent(Empty),
};
let actual = to_json_value(dummy_event).unwrap();
let expected = json!({
"content": {},
"type": "m.dummy"
});
assert_eq!(actual, expected);
}
#[test]
fn deserialization() {
let json = json!({
"content": {},
"type": "m.dummy"
});
assert!(from_json_value::<EventJson<DummyEvent>>(json)
.unwrap()
.deserialize()
.is_ok());
}
}

157
ruma-events/src/enums.rs Normal file
View File

@ -0,0 +1,157 @@
use ruma_events_macros::event_content_enum;
use serde::{Deserialize, Serialize};
use crate::{
event_kinds::{
BasicEvent, EphemeralRoomEvent, MessageEvent, MessageEventStub, StateEvent, StateEventStub,
StrippedStateEventStub, ToDeviceEvent,
},
presence::PresenceEvent,
room::redaction::{RedactionEvent, RedactionEventStub},
};
event_content_enum! {
/// Any basic event.
name: AnyBasicEventContent,
events: [
"m.direct",
"m.dummy",
"m.ignored_user_list",
"m.push_rules",
"m.room_key",
"m.tag",
]
}
event_content_enum! {
/// Any ephemeral room event.
name: AnyEphemeralRoomEventContent,
events: [
"m.fully_read",
"m.receipt",
"m.typing",
]
}
event_content_enum! {
/// Any message event.
name: AnyMessageEventContent,
events: [
"m.call.answer",
"m.call.invite",
"m.call.hangup",
"m.call.candidates",
"m.room.message",
"m.room.message.feedback",
"m.sticker",
]
}
event_content_enum! {
/// Any state event.
name: AnyStateEventContent,
events: [
"m.room.aliases",
"m.room.avatar",
"m.room.canonical_alias",
"m.room.create",
"m.room.encryption",
"m.room.guest_access",
"m.room.history_visibility",
"m.room.join_rules",
"m.room.member",
"m.room.name",
"m.room.pinned_events",
"m.room.power_levels",
"m.room.redaction",
"m.room.server_acl",
"m.room.third_party_invite",
"m.room.tombstone",
"m.room.topic",
]
}
event_content_enum! {
/// Any to-device event.
name: AnyToDeviceEventContent,
events: [
"m.dummy",
"m.room_key",
"m.room_key_request",
"m.forwarded_room_key",
"m.key.verification.request",
"m.key.verification.start",
"m.key.verification.cancel",
"m.key.verification.accept",
"m.key.verification.key",
"m.key.verification.mac",
"m.room.encrypted",
]
}
/// Any basic event, one that has no (well-known) fields outside of `content`.
pub type AnyBasicEvent = BasicEvent<AnyBasicEventContent>;
/// Any ephemeral room event.
pub type AnyEphemeralRoomEvent = EphemeralRoomEvent<AnyEphemeralRoomEventContent>;
/// Any message event.
pub type AnyMessageEvent = MessageEvent<AnyMessageEventContent>;
/// Any message event stub (message event without a `room_id`, as returned in `/sync` responses)
pub type AnyMessageEventStub = MessageEventStub<AnyMessageEventContent>;
/// Any state event.
pub type AnyStateEvent = StateEvent<AnyStateEventContent>;
/// Any state event stub (state event without a `room_id`, as returned in `/sync` responses)
pub type AnyStateEventStub = StateEventStub<AnyStateEventContent>;
/// Any stripped state event stub (stripped-down state event, as returned for rooms the user has
/// been invited to in `/sync` responses)
pub type AnyStrippedStateEventStub = StrippedStateEventStub<AnyStateEventContent>;
/// Any to-device event.
pub type AnyToDeviceEvent = ToDeviceEvent<AnyToDeviceEventContent>;
/// Any event.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum AnyEvent {
/// Any basic event.
Basic(AnyBasicEvent),
/// `"m.presence"`, the only non-room event with a `sender` field.
Presence(PresenceEvent),
/// Any ephemeral room event.
Ephemeral(AnyEphemeralRoomEvent),
/// Any message event.
Message(AnyMessageEvent),
/// `"m.room.redaction"`, the only room event with a `redacts` field.
Redaction(RedactionEvent),
/// Any state event.
State(AnyStateEvent),
}
/// Any room event.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum AnyRoomEvent {
/// Any message event.
Message(AnyMessageEvent),
/// `"m.room.redaction"`, the only room event with a `redacts` field.
Redaction(RedactionEvent),
/// Any state event.
State(AnyStateEvent),
}
/// Any room event stub (room event without a `room_id`, as returned in `/sync` responses)
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum AnyRoomEventStub {
/// Any message event stub
Message(AnyMessageEventStub),
/// `"m.room.redaction"` stub
Redaction(RedactionEventStub),
/// Any state event stub
StateEvent(AnyStateEventStub),
}

89
ruma-events/src/error.rs Normal file
View File

@ -0,0 +1,89 @@
use std::{
error::Error,
fmt::{self, Display, Formatter},
};
/// An event that is malformed or otherwise invalid.
///
/// When attempting to deserialize an [`EventJson`](enum.EventJson.html), an error in the input
/// data may cause deserialization to fail, or the JSON structure may be correct, but additional
/// constraints defined in the matrix specification are not upheld. This type provides an error
/// message and a flag for which type of error was encountered.
#[derive(Clone, Debug)]
pub struct InvalidEvent {
/// A description of the error that occurred.
pub(crate) message: String,
/// The kind of error that occurred.
pub(crate) kind: InvalidEventKind,
}
/// The kind of error that occurred.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum InvalidEventKind {
/// A deserialization error from malformed input.
Deserialization,
/// An error occurred validating input according the the matrix spec.
Validation,
}
impl InvalidEvent {
/// Constructor used in the event content macros.
///
/// This has to be public to allow the macros to be used outside of ruma-events.
#[doc(hidden)]
pub fn wrong_event_type(expected: &str, found: &str) -> Self {
Self {
message: format!("expected `{}` found {}", expected, found),
kind: InvalidEventKind::Deserialization,
}
}
/// A message describing why the event is invalid.
pub fn message(&self) -> String {
self.message.clone()
}
/// Returns whether this is a deserialization error.
pub fn is_deserialization(&self) -> bool {
self.kind == InvalidEventKind::Deserialization
}
/// Returns whether this is a validation error.
pub fn is_validation(&self) -> bool {
self.kind == InvalidEventKind::Validation
}
}
impl Display for InvalidEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message())
}
}
impl Error for InvalidEvent {}
/// An error returned when attempting to create an event with data that would make it invalid.
///
/// This type is similar to [`InvalidEvent`](struct.InvalidEvent.html), but used during the
/// construction of a new event, as opposed to deserialization of an existing event from JSON.
#[derive(Clone, Debug, PartialEq)]
pub struct InvalidInput(pub(crate) String);
impl Display for InvalidInput {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for InvalidInput {}
/// An error when attempting to create a value from a string via the `FromStr` trait.
#[derive(Clone, Copy, Eq, Debug, Hash, PartialEq)]
pub struct FromStrError;
impl Display for FromStrError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse type from string")
}
}
impl Error for FromStrError {}

View File

@ -0,0 +1,160 @@
use std::time::SystemTime;
use ruma_events_macros::Event;
use ruma_identifiers::{EventId, RoomId, UserId};
use crate::{
BasicEventContent, EphemeralRoomEventContent, EventContent, MessageEventContent,
StateEventContent, UnsignedData,
};
/// A basic event one that consists only of it's type and the `content` object.
#[derive(Clone, Debug, Event)]
pub struct BasicEvent<C: BasicEventContent> {
/// Data specific to the event type.
pub content: C,
}
/// Ephemeral room event.
#[derive(Clone, Debug, Event)]
pub struct EphemeralRoomEvent<C: EphemeralRoomEventContent> {
/// Data specific to the event type.
pub content: C,
/// The ID of the room associated with this event.
pub room_id: RoomId,
}
/// An ephemeral room event without a `room_id`.
#[derive(Clone, Debug, Event)]
pub struct EphemeralRoomEventStub<C: EphemeralRoomEventContent> {
/// Data specific to the event type.
pub content: C,
}
/// Message event.
#[derive(Clone, Debug, Event)]
pub struct MessageEvent<C: MessageEventContent> {
/// Data specific to the event type.
pub content: C,
/// The globally unique event identifier for the user who sent the event.
pub event_id: EventId,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// The ID of the room associated with this event.
pub room_id: RoomId,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// A message event without a `room_id`.
#[derive(Clone, Debug, Event)]
pub struct MessageEventStub<C: MessageEventContent> {
/// Data specific to the event type.
pub content: C,
/// The globally unique event identifier for the user who sent the event.
pub event_id: EventId,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// State event.
#[derive(Clone, Debug, Event)]
pub struct StateEvent<C: StateEventContent> {
/// Data specific to the event type.
pub content: C,
/// The globally unique event identifier for the user who sent the event.
pub event_id: EventId,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// The ID of the room associated with this event.
pub room_id: RoomId,
/// 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: String,
/// Optional previous content for this event.
pub prev_content: Option<C>,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// A state event without a `room_id`.
#[derive(Clone, Debug, Event)]
pub struct StateEventStub<C: StateEventContent> {
/// Data specific to the event type.
pub content: C,
/// The globally unique event identifier for the user who sent the event.
pub event_id: EventId,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// 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: String,
/// Optional previous content for this event.
pub prev_content: Option<C>,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// A stripped-down state event, used for previews of rooms the user has been
/// invited to.
#[derive(Clone, Debug, Event)]
pub struct StrippedStateEventStub<C: StateEventContent> {
/// Data specific to the event type.
pub content: C,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// 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: String,
}
/// An event sent using send-to-device messaging.
#[derive(Clone, Debug, Event)]
pub struct ToDeviceEvent<C: EventContent> {
/// Data specific to the event type.
pub content: C,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
}

View File

@ -0,0 +1,344 @@
use std::fmt::{Display, Formatter, Result as FmtResult};
use serde::{Deserialize, Serialize};
/// The type of an event.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(from = "String", into = "String")]
pub enum EventType {
/// m.call.answer
CallAnswer,
/// m.call.candidates
CallCandidates,
/// m.call.hangup
CallHangup,
/// m.call.invite
CallInvite,
/// m.direct
Direct,
/// m.dummy
Dummy,
/// m.forwarded_room_key
ForwardedRoomKey,
/// m.fully_read
FullyRead,
/// m.key.verification.accept
KeyVerificationAccept,
/// m.key.verification.cancel
KeyVerificationCancel,
/// m.key.verification.key
KeyVerificationKey,
/// m.key.verification.mac
KeyVerificationMac,
/// m.key.verification.request
KeyVerificationRequest,
/// m.key.verification.start
KeyVerificationStart,
/// m.ignored_user_list
IgnoredUserList,
/// m.presence
Presence,
/// m.push_rules
PushRules,
/// m.receipt
Receipt,
/// m.room.aliases
RoomAliases,
/// m.room.avatar
RoomAvatar,
/// m.room.canonical_alias
RoomCanonicalAlias,
/// m.room.create
RoomCreate,
/// m.room.encrypted
RoomEncrypted,
/// m.room.encryption
RoomEncryption,
/// m.room.guest_access
RoomGuestAccess,
/// m.room.history_visibility
RoomHistoryVisibility,
/// m.room.join_rules
RoomJoinRules,
/// m.room.member
RoomMember,
/// m.room.message
RoomMessage,
/// m.room.message.feedback
RoomMessageFeedback,
/// m.room.name
RoomName,
/// m.room.pinned_events
RoomPinnedEvents,
/// m.room.power_levels
RoomPowerLevels,
/// m.room.redaction
RoomRedaction,
/// m.room.server_acl
RoomServerAcl,
/// m.room.third_party_invite
RoomThirdPartyInvite,
/// m.room.tombstone
RoomTombstone,
/// m.room.topic
RoomTopic,
/// m.room_key
RoomKey,
/// m.room_key_request
RoomKeyRequest,
/// m.sticker
Sticker,
/// m.tag
Tag,
/// m.typing
Typing,
/// Any event that is not part of the specification.
Custom(String),
}
impl Display for EventType {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let event_type_str = match *self {
EventType::CallAnswer => "m.call.answer",
EventType::CallCandidates => "m.call.candidates",
EventType::CallHangup => "m.call.hangup",
EventType::CallInvite => "m.call.invite",
EventType::Direct => "m.direct",
EventType::Dummy => "m.dummy",
EventType::ForwardedRoomKey => "m.forwarded_room_key",
EventType::FullyRead => "m.fully_read",
EventType::KeyVerificationAccept => "m.key.verification.accept",
EventType::KeyVerificationCancel => "m.key.verification.cancel",
EventType::KeyVerificationKey => "m.key.verification.key",
EventType::KeyVerificationMac => "m.key.verification.mac",
EventType::KeyVerificationRequest => "m.key.verification.request",
EventType::KeyVerificationStart => "m.key.verification.start",
EventType::IgnoredUserList => "m.ignored_user_list",
EventType::Presence => "m.presence",
EventType::PushRules => "m.push_rules",
EventType::Receipt => "m.receipt",
EventType::RoomAliases => "m.room.aliases",
EventType::RoomAvatar => "m.room.avatar",
EventType::RoomCanonicalAlias => "m.room.canonical_alias",
EventType::RoomCreate => "m.room.create",
EventType::RoomEncrypted => "m.room.encrypted",
EventType::RoomEncryption => "m.room.encryption",
EventType::RoomGuestAccess => "m.room.guest_access",
EventType::RoomHistoryVisibility => "m.room.history_visibility",
EventType::RoomJoinRules => "m.room.join_rules",
EventType::RoomMember => "m.room.member",
EventType::RoomMessage => "m.room.message",
EventType::RoomMessageFeedback => "m.room.message.feedback",
EventType::RoomName => "m.room.name",
EventType::RoomPinnedEvents => "m.room.pinned_events",
EventType::RoomPowerLevels => "m.room.power_levels",
EventType::RoomRedaction => "m.room.redaction",
EventType::RoomServerAcl => "m.room.server_acl",
EventType::RoomThirdPartyInvite => "m.room.third_party_invite",
EventType::RoomTombstone => "m.room.tombstone",
EventType::RoomTopic => "m.room.topic",
EventType::RoomKey => "m.room_key",
EventType::RoomKeyRequest => "m.room_key_request",
EventType::Sticker => "m.sticker",
EventType::Tag => "m.tag",
EventType::Typing => "m.typing",
EventType::Custom(ref event_type) => event_type,
};
write!(f, "{}", event_type_str)
}
}
impl<T> From<T> for EventType
where
T: Into<String> + AsRef<str>,
{
fn from(s: T) -> EventType {
match s.as_ref() {
"m.call.answer" => EventType::CallAnswer,
"m.call.candidates" => EventType::CallCandidates,
"m.call.hangup" => EventType::CallHangup,
"m.call.invite" => EventType::CallInvite,
"m.direct" => EventType::Direct,
"m.dummy" => EventType::Dummy,
"m.forwarded_room_key" => EventType::ForwardedRoomKey,
"m.fully_read" => EventType::FullyRead,
"m.key.verification.accept" => EventType::KeyVerificationAccept,
"m.key.verification.cancel" => EventType::KeyVerificationCancel,
"m.key.verification.key" => EventType::KeyVerificationKey,
"m.key.verification.mac" => EventType::KeyVerificationMac,
"m.key.verification.request" => EventType::KeyVerificationRequest,
"m.key.verification.start" => EventType::KeyVerificationStart,
"m.ignored_user_list" => EventType::IgnoredUserList,
"m.presence" => EventType::Presence,
"m.push_rules" => EventType::PushRules,
"m.receipt" => EventType::Receipt,
"m.room.aliases" => EventType::RoomAliases,
"m.room.avatar" => EventType::RoomAvatar,
"m.room.canonical_alias" => EventType::RoomCanonicalAlias,
"m.room.create" => EventType::RoomCreate,
"m.room.encrypted" => EventType::RoomEncrypted,
"m.room.encryption" => EventType::RoomEncryption,
"m.room.guest_access" => EventType::RoomGuestAccess,
"m.room.history_visibility" => EventType::RoomHistoryVisibility,
"m.room.join_rules" => EventType::RoomJoinRules,
"m.room.member" => EventType::RoomMember,
"m.room.message" => EventType::RoomMessage,
"m.room.message.feedback" => EventType::RoomMessageFeedback,
"m.room.name" => EventType::RoomName,
"m.room.pinned_events" => EventType::RoomPinnedEvents,
"m.room.power_levels" => EventType::RoomPowerLevels,
"m.room.redaction" => EventType::RoomRedaction,
"m.room.server_acl" => EventType::RoomServerAcl,
"m.room.third_party_invite" => EventType::RoomThirdPartyInvite,
"m.room.tombstone" => EventType::RoomTombstone,
"m.room.topic" => EventType::RoomTopic,
"m.room_key" => EventType::RoomKey,
"m.room_key_request" => EventType::RoomKeyRequest,
"m.sticker" => EventType::Sticker,
"m.tag" => EventType::Tag,
"m.typing" => EventType::Typing,
_ => EventType::Custom(s.into()),
}
}
}
impl From<EventType> for String {
fn from(event_type: EventType) -> String {
event_type.to_string()
}
}
#[cfg(test)]
mod tests {
use ruma_serde::test::serde_json_eq;
use serde_json::json;
use super::*;
#[allow(clippy::cognitive_complexity)]
#[test]
fn serialize_and_deserialize_from_display_form() {
serde_json_eq(EventType::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(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(
EventType::KeyVerificationAccept,
json!("m.key.verification.accept"),
);
serde_json_eq(
EventType::KeyVerificationCancel,
json!("m.key.verification.cancel"),
);
serde_json_eq(
EventType::KeyVerificationKey,
json!("m.key.verification.key"),
);
serde_json_eq(
EventType::KeyVerificationMac,
json!("m.key.verification.mac"),
);
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::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(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::RoomMessageFeedback,
json!("m.room.message.feedback"),
);
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(
EventType::Custom("io.ruma.test".to_string()),
json!("io.ruma.test"),
);
}
}

View File

@ -0,0 +1,48 @@
//! Types for the *m.forwarded_room_key* event.
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::RoomId;
use serde::{Deserialize, Serialize};
use super::Algorithm;
use crate::BasicEvent;
/// This event type is used to forward keys for end-to-end encryption.
///
/// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event.
pub type ForwardedRoomKeyEvent = BasicEvent<ForwardedRoomKeyEventContent>;
/// The payload for `ForwardedRoomKeyEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.forwarded_room_key")]
pub struct ForwardedRoomKeyEventContent {
/// The encryption algorithm the key in this event is to be used with.
pub algorithm: Algorithm,
/// The room where the key is used.
pub room_id: RoomId,
/// 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<String>,
}

View File

@ -0,0 +1,21 @@
//! Types for the *m.fully_read* event.
use ruma_events_macros::EphemeralRoomEventContent;
use ruma_identifiers::EventId;
use serde::{Deserialize, Serialize};
use crate::EphemeralRoomEvent;
/// 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.
pub type FullyReadEvent = EphemeralRoomEvent<FullyReadEventContent>;
/// The payload for `FullyReadEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, EphemeralRoomEventContent)]
#[ruma_event(type = "m.fully_read")]
pub struct FullyReadEventContent {
/// The event the user's read marker is located at in the room.
pub event_id: EventId,
}

View File

@ -0,0 +1,73 @@
//! Types for the *m.ignored_user_list* event.
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// A list of users to ignore.
pub type IgnoredUserListEvent = BasicEvent<IgnoredUserListEventContent>;
/// The payload for `IgnoredUserListEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.ignored_user_list")]
pub struct IgnoredUserListEventContent {
/// A list of users to ignore.
#[serde(with = "ruma_serde::vec_as_map_of_empty")]
pub ignored_users: Vec<UserId>,
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use matches::assert_matches;
use ruma_identifiers::UserId;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::IgnoredUserListEventContent;
use crate::{AnyBasicEventContent, BasicEvent, EventJson};
#[test]
fn serialization() {
let ignored_user_list_event = BasicEvent {
content: IgnoredUserListEventContent {
ignored_users: vec![UserId::try_from("@carl:example.com").unwrap()],
},
};
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"
});
assert_matches!(
from_json_value::<EventJson<BasicEvent<AnyBasicEventContent>>>(json)
.unwrap()
.deserialize()
.unwrap(),
BasicEvent {
content: AnyBasicEventContent::IgnoredUserList(IgnoredUserListEventContent { ignored_users, }),
} if ignored_users == vec![UserId::try_from("@carl:example.com").unwrap()]
);
}
}

124
ruma-events/src/json.rs Normal file
View File

@ -0,0 +1,124 @@
use std::{
clone::Clone,
fmt::{self, Debug, Formatter},
marker::PhantomData,
};
use serde::{
de::{Deserialize, DeserializeOwned, Deserializer},
ser::{Serialize, Serializer},
};
use serde_json::value::RawValue;
use crate::{
error::{InvalidEvent, InvalidEventKind},
EventContent,
};
/// A wrapper around `Box<RawValue>`, to be used in place of event [content] [collection] types in
/// Matrix endpoint definition to allow request and response types to contain unknown events in
/// addition to the known event(s) represented by the generic argument `Ev`.
pub struct EventJson<T> {
json: Box<RawValue>,
_ev: PhantomData<T>,
}
impl<T> EventJson<T> {
fn new(json: Box<RawValue>) -> Self {
Self {
json,
_ev: PhantomData,
}
}
/// Create an `EventJson` from a boxed `RawValue`.
pub fn from_json(raw: Box<RawValue>) -> Self {
Self::new(raw)
}
/// Access the underlying json value.
pub fn json(&self) -> &RawValue {
&self.json
}
/// Convert `self` into the underlying json value.
pub fn into_json(self) -> Box<RawValue> {
self.json
}
}
impl<T> EventJson<T>
where
T: DeserializeOwned,
{
/// Try to deserialize the JSON into the expected event type.
pub fn deserialize(&self) -> Result<T, InvalidEvent> {
match serde_json::from_str(self.json.get()) {
Ok(value) => Ok(value),
Err(err) => Err(InvalidEvent {
message: err.to_string(),
kind: InvalidEventKind::Validation,
}),
}
}
}
impl<T: EventContent> EventJson<T>
where
T: EventContent,
{
/// Try to deserialize the JSON as event content
pub fn deserialize_content(self, event_type: &str) -> Result<T, InvalidEvent> {
T::from_parts(event_type, self.json).map_err(|err| InvalidEvent {
message: err,
kind: InvalidEventKind::Deserialization,
})
}
}
impl<T: Serialize> From<&T> for EventJson<T> {
fn from(val: &T) -> Self {
Self::new(serde_json::value::to_raw_value(val).unwrap())
}
}
// With specialization a fast path from impl for `impl<T> From<Box<RawValue...`
// could be used. Until then there is a special constructor `from_json` for this.
impl<T: Serialize> From<T> for EventJson<T> {
fn from(val: T) -> Self {
Self::from(&val)
}
}
impl<T> Clone for EventJson<T> {
fn clone(&self) -> Self {
Self::new(self.json.clone())
}
}
impl<T> Debug for EventJson<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
use std::any::type_name;
f.debug_struct(&format!("EventJson::<{}>", type_name::<T>()))
.field("json", &self.json)
.finish()
}
}
impl<'de, T> Deserialize<'de> for EventJson<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Box::<RawValue>::deserialize(deserializer).map(Self::new)
}
}
impl<T> Serialize for EventJson<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.json.serialize(serializer)
}
}

3
ruma-events/src/key.rs Normal file
View File

@ -0,0 +1,3 @@
//! Modules for events in the *m.key* namespace.
pub mod verification;

View File

@ -0,0 +1,61 @@
//! Modules for events in the *m.key.verification* namespace.
//!
//! This module also contains types shared by events in its child namespaces.
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
pub mod accept;
pub mod cancel;
pub mod key;
pub mod mac;
pub mod request;
pub mod start;
/// A hash algorithm.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum HashAlgorithm {
/// The SHA256 hash algorithm.
Sha256,
}
/// A key agreement protocol.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum KeyAgreementProtocol {
/// The [Curve25519](https://cr.yp.to/ecdh.html) key agreement protocol.
Curve25519,
}
/// A message authentication code algorithm.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum MessageAuthenticationCode {
/// The HKDF-HMAC-SHA256 MAC.
HkdfHmacSha256,
}
/// A Short Authentication String method.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ShortAuthenticationString {
/// The decimal method.
Decimal,
/// The emoji method.
Emoji,
}
/// A Short Authentication String (SAS) verification method.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
pub enum VerificationMethod {
/// The *m.sas.v1* verification method.
#[serde(rename = "m.sas.v1")]
#[strum(serialize = "m.sas.v1")]
MSasV1,
}

View File

@ -0,0 +1,52 @@
//! Types for the *m.key.verification.accept* event.
use ruma_events_macros::BasicEventContent;
use serde::{Deserialize, Serialize};
use super::{
HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString,
VerificationMethod,
};
use crate::BasicEvent;
/// Accepts a previously sent *m.key.verification.start* message.
///
/// Typically sent as a to-device event.
pub type AcceptEvent = BasicEvent<AcceptEventContent>;
/// The payload for `AcceptEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.key.verification.accept")]
pub struct AcceptEventContent {
/// 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: String,
/// The verification method to use.
///
/// Must be `m.sas.v1`.
pub method: VerificationMethod,
/// 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<ShortAuthenticationString>,
/// 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: String,
}

View File

@ -0,0 +1,155 @@
//! Types for the *m.key.verification.cancel* event.
use std::fmt::{Display, Formatter, Result as FmtResult};
use ruma_events_macros::BasicEventContent;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// Cancels a key verification process/request.
///
/// Typically sent as a to-device event.
pub type CancelEvent = BasicEvent<CancelEventContent>;
/// The payload for `CancelEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.key.verification.cancel")]
pub struct CancelEventContent {
/// The opaque identifier for the verification process/request.
pub transaction_id: String,
/// 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,
}
/// An error code for why the process/request was cancelled by the user.
///
/// Custom error codes should use the Java package naming convention.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum CancelCode {
/// The user cancelled the verification.
User,
/// The verification process timed out. Verification processes can define their own timeout
/// parameters.
Timeout,
/// The device does not know about the given transaction ID.
UnknownTransaction,
/// The device does not know how to handle the requested method.
///
/// This should be sent for *m.key.verification.start* messages and messages defined by
/// individual verification processes.
UnknownMethod,
/// The device received an unexpected message.
///
/// Typically raised when one of the parties is handling the verification out of order.
UnexpectedMessage,
/// The key was not verified.
KeyMismatch,
/// The expected user did not match the user verified.
UserMismatch,
/// The message received was invalid.
InvalidMessage,
/// An *m.key.verification.request* was accepted by a different device.
///
/// The device receiving this error can ignore the verification request.
Accepted,
/// Any code that is not part of the specification.
Custom(String),
}
impl Display for CancelCode {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let cancel_code_str = match *self {
CancelCode::User => "m.user",
CancelCode::Timeout => "m.timeout",
CancelCode::UnknownTransaction => "m.unknown_transaction",
CancelCode::UnknownMethod => "m.unknown_method",
CancelCode::UnexpectedMessage => "m.unexpected_message",
CancelCode::KeyMismatch => "m.key_mismatch",
CancelCode::UserMismatch => "m.user_mismatch",
CancelCode::InvalidMessage => "m.invalid_message",
CancelCode::Accepted => "m.accepted",
CancelCode::Custom(ref cancel_code) => cancel_code,
};
write!(f, "{}", cancel_code_str)
}
}
impl<T> From<T> for CancelCode
where
T: Into<String> + AsRef<str>,
{
fn from(s: T) -> CancelCode {
match s.as_ref() {
"m.user" => CancelCode::User,
"m.timeout" => CancelCode::Timeout,
"m.unknown_transaction" => CancelCode::UnknownTransaction,
"m.unknown_method" => CancelCode::UnknownMethod,
"m.unexpected_message" => CancelCode::UnexpectedMessage,
"m.key_mismatch" => CancelCode::KeyMismatch,
"m.user_mismatch" => CancelCode::UserMismatch,
"m.invalid_message" => CancelCode::InvalidMessage,
"m.accepted" => CancelCode::Accepted,
_ => CancelCode::Custom(s.into()),
}
}
}
impl From<CancelCode> for String {
fn from(cancel_code: CancelCode) -> String {
cancel_code.to_string()
}
}
#[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::Custom("io.ruma.test".to_string())).unwrap(),
json!("io.ruma.test")
);
}
#[test]
fn cancel_codes_deserialize_from_display_form() {
assert_eq!(
from_json_value::<CancelCode>(json!("m.user")).unwrap(),
CancelCode::User
);
}
#[test]
fn custom_cancel_codes_deserialize_from_display_form() {
assert_eq!(
from_json_value::<CancelCode>(json!("io.ruma.test")).unwrap(),
CancelCode::Custom("io.ruma.test".to_string())
)
}
}

View File

@ -0,0 +1,24 @@
//! Types for the *m.key.verification.key* event.
use ruma_events_macros::BasicEventContent;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// Sends the ephemeral public key for a device to the partner device.
///
/// Typically sent as a to-device event.
pub type KeyEvent = BasicEvent<KeyEventContent>;
/// The payload for `KeyEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.key.verification.key")]
pub struct KeyEventContent {
/// 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: String,
/// The device's ephemeral public key, encoded as unpadded Base64.
pub key: String,
}

View File

@ -0,0 +1,32 @@
//! Types for the *m.key.verification.mac* event.
use std::collections::BTreeMap;
use ruma_events_macros::BasicEventContent;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// Sends the MAC of a device's key to the partner device.
///
/// Typically sent as a to-device event.
pub type MacEvent = BasicEvent<MacEventContent>;
/// The payload for `MacEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.key.verification.mac")]
pub struct MacEventContent {
/// 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: String,
/// 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<String, String>,
/// The MAC of the comma-separated, sorted, list of key IDs given in the `mac` property, encoded
/// as unpadded Base64.
pub keys: String,
}

View File

@ -0,0 +1,38 @@
//! Types for the *m.key.verification.request* event.
use std::time::SystemTime;
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::DeviceId;
use serde::{Deserialize, Serialize};
use super::VerificationMethod;
use crate::BasicEvent;
/// Requests a key verification with another user's devices.
///
/// Typically sent as a to-device event.
pub type RequestEvent = BasicEvent<RequestEventContent>;
/// The payload for `RequestEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.key.verification.request")]
pub struct RequestEventContent {
/// The device ID which is initiating the request.
pub from_device: DeviceId,
/// An opaque identifier for the verification request.
///
/// Must be unique with respect to the devices involved.
pub transaction_id: String,
/// The verification methods supported by the sender.
pub methods: Vec<VerificationMethod>,
/// 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.
#[serde(with = "ruma_serde::time::ms_since_unix_epoch")]
pub timestamp: SystemTime,
}

View File

@ -0,0 +1,464 @@
//! Types for the *m.key.verification.start* event.
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::DeviceId;
use serde::{Deserialize, Serialize};
use super::{
HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString,
};
use crate::{BasicEvent, InvalidInput};
/// Begins an SAS key verification process.
///
/// Typically sent as a to-device event.
pub type StartEvent = BasicEvent<StartEventContent>;
/// The payload of an *m.key.verification.start* event.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.key.verification.start")]
#[serde(tag = "method")]
pub enum StartEventContent {
/// The *m.sas.v1* verification method.
#[serde(rename = "m.sas.v1")]
MSasV1(MSasV1Content),
}
/// The payload of an *m.key.verification.start* event using the *m.sas.v1* method.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MSasV1Content {
/// The device ID which is initiating the process.
pub(crate) from_device: DeviceId,
/// 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(crate) transaction_id: String,
/// The key agreement protocols the sending device understands.
///
/// Must include at least `curve25519`.
pub(crate) key_agreement_protocols: Vec<KeyAgreementProtocol>,
/// The hash methods the sending device understands.
///
/// Must include at least `sha256`.
pub(crate) hashes: Vec<HashAlgorithm>,
/// The message authentication codes that the sending device understands.
///
/// Must include at least `hkdf-hmac-sha256`.
pub(crate) message_authentication_codes: Vec<MessageAuthenticationCode>,
/// The SAS methods the sending device (and the sending device's user) understands.
///
/// Must include at least `decimal`. Optionally can include `emoji`.
pub(crate) short_authentication_string: Vec<ShortAuthenticationString>,
}
/// Options for creating an `MSasV1Content` with `MSasV1Content::new`.
#[derive(Clone, Debug, Deserialize)]
pub struct MSasV1ContentOptions {
/// The device ID which is initiating the process.
pub from_device: DeviceId,
/// 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: String,
/// The key agreement protocols the sending device understands.
///
/// Must include at least `curve25519`.
pub key_agreement_protocols: Vec<KeyAgreementProtocol>,
/// The hash methods the sending device understands.
///
/// Must include at least `sha256`.
pub hashes: Vec<HashAlgorithm>,
/// The message authentication codes that the sending device understands.
///
/// Must include at least `hkdf-hmac-sha256`.
pub message_authentication_codes: Vec<MessageAuthenticationCode>,
/// 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<ShortAuthenticationString>,
}
impl MSasV1Content {
/// Create a new `MSasV1Content` with the given values.
///
/// # Errors
///
/// `InvalidInput` will be returned in the following cases:
///
/// * `key_agreement_protocols` does not include `KeyAgreementProtocol::Curve25519`.
/// * `hashes` does not include `HashAlgorithm::Sha256`.
/// * `message_authentication_codes` does not include
/// `MessageAuthenticationCode::HkdfHmacSha256`.
/// * `short_authentication_string` does not include `ShortAuthenticationString::Decimal`.
pub fn new(options: MSasV1ContentOptions) -> Result<Self, InvalidInput> {
if !options
.key_agreement_protocols
.contains(&KeyAgreementProtocol::Curve25519)
{
return Err(InvalidInput("`key_agreement_protocols` must contain at least `KeyAgreementProtocol::Curve25519`".to_string()));
}
if !options.hashes.contains(&HashAlgorithm::Sha256) {
return Err(InvalidInput(
"`hashes` must contain at least `HashAlgorithm::Sha256`".to_string(),
));
}
if !options
.message_authentication_codes
.contains(&MessageAuthenticationCode::HkdfHmacSha256)
{
return Err(InvalidInput("`message_authentication_codes` must contain at least `MessageAuthenticationCode::HkdfHmacSha256`".to_string()));
}
if !options
.short_authentication_string
.contains(&ShortAuthenticationString::Decimal)
{
return Err(InvalidInput("`short_authentication_string` must contain at least `ShortAuthenticationString::Decimal`".to_string()));
}
Ok(Self {
from_device: options.from_device,
transaction_id: options.transaction_id,
key_agreement_protocols: options.key_agreement_protocols,
hashes: options.hashes,
message_authentication_codes: options.message_authentication_codes,
short_authentication_string: options.short_authentication_string,
})
}
}
#[cfg(test)]
mod tests {
use matches::assert_matches;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{
HashAlgorithm, KeyAgreementProtocol, MSasV1Content, MSasV1ContentOptions,
MessageAuthenticationCode, ShortAuthenticationString, StartEvent, StartEventContent,
};
use crate::EventJson;
#[test]
fn invalid_m_sas_v1_content_missing_required_key_agreement_protocols() {
let error = MSasV1Content::new(MSasV1ContentOptions {
from_device: "123".to_string(),
transaction_id: "456".to_string(),
hashes: vec![HashAlgorithm::Sha256],
key_agreement_protocols: vec![],
message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256],
short_authentication_string: vec![ShortAuthenticationString::Decimal],
})
.err()
.unwrap();
assert!(error.to_string().contains("key_agreement_protocols"));
}
#[test]
fn invalid_m_sas_v1_content_missing_required_hashes() {
let error = MSasV1Content::new(MSasV1ContentOptions {
from_device: "123".to_string(),
transaction_id: "456".to_string(),
hashes: vec![],
key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256],
short_authentication_string: vec![ShortAuthenticationString::Decimal],
})
.err()
.unwrap();
assert!(error.to_string().contains("hashes"));
}
#[test]
fn invalid_m_sas_v1_content_missing_required_message_authentication_codes() {
let error = MSasV1Content::new(MSasV1ContentOptions {
from_device: "123".to_string(),
transaction_id: "456".to_string(),
hashes: vec![HashAlgorithm::Sha256],
key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
message_authentication_codes: vec![],
short_authentication_string: vec![ShortAuthenticationString::Decimal],
})
.err()
.unwrap();
assert!(error.to_string().contains("message_authentication_codes"));
}
#[test]
fn invalid_m_sas_v1_content_missing_required_short_authentication_string() {
let error = MSasV1Content::new(MSasV1ContentOptions {
from_device: "123".to_string(),
transaction_id: "456".to_string(),
hashes: vec![HashAlgorithm::Sha256],
key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256],
short_authentication_string: vec![],
})
.err()
.unwrap();
assert!(error.to_string().contains("short_authentication_string"));
}
#[test]
fn serialization() {
let key_verification_start_content = StartEventContent::MSasV1(
MSasV1Content::new(MSasV1ContentOptions {
from_device: "123".to_string(),
transaction_id: "456".to_string(),
hashes: vec![HashAlgorithm::Sha256],
key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256],
short_authentication_string: vec![ShortAuthenticationString::Decimal],
})
.unwrap(),
);
let key_verification_start = StartEvent {
content: key_verification_start_content,
};
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"
});
assert_eq!(to_json_value(&key_verification_start).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.
assert_matches!(
from_json_value::<EventJson<StartEventContent>>(json)
.unwrap()
.deserialize()
.unwrap(),
StartEventContent::MSasV1(MSasV1Content {
from_device,
transaction_id,
hashes,
key_agreement_protocols,
message_authentication_codes,
short_authentication_string,
}) if from_device == "123"
&& transaction_id == "456"
&& hashes == vec![HashAlgorithm::Sha256]
&& key_agreement_protocols == vec![KeyAgreementProtocol::Curve25519]
&& message_authentication_codes == vec![MessageAuthenticationCode::HkdfHmacSha256]
&& 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"
});
assert_matches!(
from_json_value::<EventJson<StartEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
StartEvent {
content: StartEventContent::MSasV1(MSasV1Content {
from_device,
transaction_id,
hashes,
key_agreement_protocols,
message_authentication_codes,
short_authentication_string,
})
} if from_device == "123"
&& transaction_id == "456"
&& hashes == vec![HashAlgorithm::Sha256]
&& key_agreement_protocols == vec![KeyAgreementProtocol::Curve25519]
&& message_authentication_codes == vec![MessageAuthenticationCode::HkdfHmacSha256]
&& short_authentication_string == vec![ShortAuthenticationString::Decimal]
)
}
#[test]
fn deserialization_failure() {
// Ensure that invalid JSON creates a `serde_json::Error` and not `InvalidEvent`
assert!(serde_json::from_str::<EventJson<StartEventContent>>("{").is_err());
}
// TODO this fails because the error is a Validation error not deserialization?
/*
#[test]
fn deserialization_structure_mismatch() {
// Missing several required fields.
let error =
from_json_value::<EventJson<StartEventContent>>(json!({ "from_device": "123" }))
.unwrap()
.deserialize()
.unwrap_err();
assert!(error.message().contains("missing field"));
assert!(error.is_deserialization());
}
*/
// TODO re implement validation done in TryFromRaw else where
/*
#[test]
fn deserialization_validation_missing_required_key_agreement_protocols() {
let json_data = json!({
"from_device": "123",
"transaction_id": "456",
"method": "m.sas.v1",
"key_agreement_protocols": [],
"hashes": ["sha256"],
"message_authentication_codes": ["hkdf-hmac-sha256"],
"short_authentication_string": ["decimal"]
});
let error = from_json_value::<EventJson<StartEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap_err();
assert!(error.message().contains("key_agreement_protocols"));
assert!(error.is_validation());
}
*/
// TODO re implement validation done in TryFromRaw else where
/*
#[test]
fn deserialization_validation_missing_required_hashes() {
let json_data = json!({
"from_device": "123",
"transaction_id": "456",
"method": "m.sas.v1",
"key_agreement_protocols": ["curve25519"],
"hashes": [],
"message_authentication_codes": ["hkdf-hmac-sha256"],
"short_authentication_string": ["decimal"]
});
let error = from_json_value::<EventJson<StartEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap_err();
assert!(error.message().contains("hashes"));
assert!(error.is_validation());
}
*/
// TODO re implement validation done in TryFromRaw else where
/*
#[test]
fn deserialization_validation_missing_required_message_authentication_codes() {
let json_data = json!({
"from_device": "123",
"transaction_id": "456",
"method": "m.sas.v1",
"key_agreement_protocols": ["curve25519"],
"hashes": ["sha256"],
"message_authentication_codes": [],
"short_authentication_string": ["decimal"]
});
let error = from_json_value::<EventJson<StartEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap_err();
assert!(error.message().contains("message_authentication_codes"));
assert!(error.is_validation());
}
*/
/*
#[test]
fn deserialization_validation_missing_required_short_authentication_string() {
let json_data = json!({
"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": []
});
let error = from_json_value::<EventJson<StartEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap_err();
assert!(error.message().contains("short_authentication_string"));
assert!(error.is_validation());
}
*/
// TODO re implement validation done in TryFromRaw else where
/*
#[test]
fn deserialization_of_event_validates_content() {
// This JSON is missing the required value of "curve25519" for "key_agreement_protocols".
let json_data = json!({
"content": {
"from_device": "123",
"transaction_id": "456",
"method": "m.sas.v1",
"key_agreement_protocols": [],
"hashes": ["sha256"],
"message_authentication_codes": ["hkdf-hmac-sha256"],
"short_authentication_string": ["decimal"]
},
"type": "m.key.verification.start"
});
let error = from_json_value::<EventJson<StartEvent>>(json_data)
.unwrap()
.deserialize()
.unwrap_err();
assert!(error.message().contains("key_agreement_protocols"));
assert!(error.is_validation());
}
**/
}

234
ruma-events/src/lib.rs Normal file
View File

@ -0,0 +1,234 @@
//! Crate `ruma_events` contains serializable types for the events in the [Matrix](https://matrix.org)
//! specification that can be shared by client and server code.
//!
//! 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, and Matrix clients
//! and servers must understand their semantics.
//! ruma-events contains Rust types for each of the event types defined by the specification and
//! facilities for extending the event system for custom event types.
//!
//! # Event types
//!
//! ruma-events includes a Rust enum called `EventType`, which provides a simple enumeration of
//! all the event types defined by the Matrix specification. Matrix event types are serialized to
//! JSON strings in [reverse domain name
//! notation](https://en.wikipedia.org/wiki/Reverse_domain_name_notation), although the core event
//! types all use the special "m" TLD, e.g. *m.room.message*.
//! `EventType` also includes a variant called `Custom`, which is a catch-all that stores a string
//! containing the name of any event type that isn't part of the specification.
//! `EventType` is used throughout ruma-events to identify and differentiate between events of
//! different types.
//!
//! # Event kinds
//!
//! Matrix defines three "kinds" of events:
//!
//! 1. **Events**, which are arbitrary JSON structures that have two required keys:
//! * `type`, which specifies the event's type
//! * `content`, which is a JSON object containing the "payload" of the event
//! 2. **Room events**, which are a superset of events and represent actions that occurred within
//! the context of a Matrix room.
//! They have at least the following additional keys:
//! * `event_id`, which is a unique identifier for the event
//! * `room_id`, which is a unique identifier for the room in which the event occurred
//! * `sender`, which is the unique identifier of the Matrix user who created the event
//! * Optionally, `unsigned`, which is a JSON object containing arbitrary additional metadata
//! that is not digitally signed by Matrix homeservers.
//! 3. **State events**, which are a superset of room events and represent persistent state
//! specific to a room, such as the room's member list or topic.
//! Within a single room, state events of the same type and with the same "state key" will
//! effectively "replace" the previous one, updating the room's state.
//! They have at least the following additional keys:
//! * `state_key`, a string which serves as a sort of "sub-type."
//! The state key allows a room to persist multiple state events of the same type.
//! You can think of a room's state events as being a `BTreeMap` where the keys are the tuple
//! `(event_type, state_key)`.
//! * Optionally, `prev_content`, a JSON object containing the `content` object from the
//! previous event of the given `(event_type, state_key)` tuple in the given room.
//!
//! ruma-events represents these three event kinds as traits, allowing any Rust type to serve as a
//! Matrix event so long as it upholds the contract expected of its kind.
//!
//! # Core event types
//!
//! ruma-events includes Rust types for every one of the 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::MessageEvent`.
//! 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.
//!
//! # Custom event types
//!
//! Although any Rust type that implements `Event`, `RoomEvent`, or `StateEvent` can serve as a
//! Matrix event type, ruma-events also includes a few convenience types for representing events
//! that are not covered by the spec and not otherwise known by the application.
//! `CustomEvent`, `CustomRoomEvent`, and `CustomStateEvent` are simple implementations of their
//! respective event traits whose `content` field is simply a `serde_json::Value` value, which
//! represents arbitrary JSON.
//!
//! # Serialization and deserialization
//!
//! All concrete event types in ruma-events can be serialized via the `Serialize` trait from
//! [serde](https://serde.rs/) and can be deserialized from as `EventJson<EventType>`. In order to
//! handle incoming data that may not conform to `ruma-events`' strict definitions of event
//! structures, deserialization will return `EventJson::Err` on error. This error covers both
//! structurally invalid JSON data as well as structurally valid JSON that doesn't fulfill
//! additional constraints the matrix specification defines for some event types. The error exposes
//! the deserialized `serde_json::Value` so that developers can still work with the received
//! event data. This makes it possible to deserialize a collection of events without the entire
//! collection failing to deserialize due to a single invalid event. The "content" type for each
//! event also implements `Serialize` and either `TryFromRaw` (enabling usage as
//! `EventJson<ContentType>` for dedicated content types) or `Deserialize` (when the content is a
//! type alias), allowing content to be converted to and from JSON indepedently of the surrounding
//! event structure, if needed.
//!
//! # Collections
//!
//! With the trait-based approach to events, it's easy to write generic collection types like
//! `Vec<Box<R: RoomEvent>>`.
//! However, there are APIs in the Matrix specification that involve heterogeneous collections of
//! events, i.e. a list of events of different event types.
//! Because Rust does not have a facility for arrays, vectors, or slices containing multiple
//! concrete types, ruma-events provides special collection types for this purpose.
//! The collection types are enums which effectively "wrap" each possible event type of a
//! particular event "kind."
//!
//! Because of the hierarchical nature of event kinds in Matrix, these collection types are divied
//! into two modules, `ruma_events::collections::all` and `ruma_events::collections::only`.
//! The "all" versions include every event type that implements the relevant event trait as well as
//! more specific event traits.
//! The "only" versions include only the event types that implement "at most" the relevant event
//! trait.
//!
//! For example, the `ruma_events::collections::all::Event` enum includes *m.room.message*, because
//! that event type is both an event and a room event.
//! However, the `ruma_events::collections::only::Event` enum does *not* include *m.room.message*,
//! because *m.room.message* implements a *more specific* event trait than `Event`.
#![recursion_limit = "1024"]
#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)]
// Since we support Rust 1.36.0, we can't apply this suggestion yet
#![allow(clippy::use_self)]
use std::fmt::Debug;
use js_int::Int;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use self::room::redaction::RedactionEvent;
#[deprecated = "Use ruma_serde::empty::Empty directly instead."]
pub use ruma_serde::empty::Empty;
mod algorithm;
mod enums;
mod error;
mod event_kinds;
mod event_type;
mod json;
#[doc(hidden)] // only public for external tests
pub mod util;
// Hack to allow both ruma-events itself and external crates (or tests) to use procedural macros
// that expect `ruma_events` to exist in the prelude.
extern crate self as ruma_events;
pub mod call;
pub mod custom;
pub mod direct;
pub mod dummy;
pub mod forwarded_room_key;
pub mod fully_read;
pub mod ignored_user_list;
pub mod key;
pub mod presence;
pub mod push_rules;
pub mod receipt;
pub mod room;
pub mod room_key;
pub mod room_key_request;
pub mod sticker;
pub mod tag;
pub mod typing;
pub use self::{
algorithm::Algorithm,
custom::{CustomBasicEvent, CustomMessageEvent, CustomStateEvent},
enums::{
AnyBasicEvent, AnyBasicEventContent, AnyEphemeralRoomEvent, AnyEphemeralRoomEventContent,
AnyEvent, AnyMessageEvent, AnyMessageEventContent, AnyMessageEventStub, AnyRoomEvent,
AnyRoomEventStub, AnyStateEvent, AnyStateEventContent, AnyStateEventStub,
AnyStrippedStateEventStub, AnyToDeviceEvent, AnyToDeviceEventContent,
},
error::{FromStrError, InvalidEvent, InvalidInput},
event_kinds::{
BasicEvent, EphemeralRoomEvent, EphemeralRoomEventStub, MessageEvent, MessageEventStub,
StateEvent, StateEventStub, StrippedStateEventStub, ToDeviceEvent,
},
event_type::EventType,
json::EventJson,
};
/// Extra information about an event that is not incorporated into the event's
/// hash.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct UnsignedData {
/// 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<Int>,
/// The event that redacted this event, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub redacted_because: Option<EventJson<RedactionEvent>>,
/// 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<String>,
}
impl UnsignedData {
/// 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.
pub fn is_empty(&self) -> bool {
self.age.is_none() && self.transaction_id.is_none() && self.redacted_because.is_none()
}
}
/// The base trait that all event content types implement.
///
/// Implementing this trait allows content types to be serialized as well as deserialized.
pub trait EventContent: Sized + Serialize {
/// A matrix event identifier, like `m.room.message`.
fn event_type(&self) -> &str;
/// Constructs the given event content.
fn from_parts(event_type: &str, content: Box<RawJsonValue>) -> Result<Self, String>;
}
/// Marker trait for the content of an ephemeral room event.
pub trait EphemeralRoomEventContent: EventContent {}
/// Marker trait for the content of a basic event.
pub trait BasicEventContent: EventContent {}
/// Marker trait for the content of a room event.
pub trait RoomEventContent: EventContent {}
/// Marker trait for the content of a message event.
pub trait MessageEventContent: RoomEventContent {}
/// Marker trait for the content of a state event.
pub trait StateEventContent: RoomEventContent {}

128
ruma-events/src/presence.rs Normal file
View File

@ -0,0 +1,128 @@
//! 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;
pub use ruma_common::presence::PresenceState;
use ruma_events_macros::{Event, EventContent};
use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
/// Presence event.
#[derive(Clone, Debug, Event)]
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: UserId,
}
/// Informs the room of members presence.
///
/// This is the only event content a `PresenceEvent` can contain as it's
/// `content` field.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.presence")]
pub struct PresenceEventContent {
/// The current avatar URL for this user.
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
/// Whether or not the user is currently active.
#[serde(skip_serializing_if = "Option::is_none")]
pub currently_active: Option<bool>,
/// The current display name for this user.
#[serde(skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
/// The last time since this user performed some action, in milliseconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_active_ago: Option<UInt>,
/// 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<String>,
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use js_int::UInt;
use matches::assert_matches;
use ruma_identifiers::UserId;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{PresenceEvent, PresenceEventContent, PresenceState};
use crate::EventJson;
#[test]
fn serialization() {
let event = PresenceEvent {
content: PresenceEventContent {
avatar_url: Some("mxc://localhost:wefuiwegh8742w".to_string()),
currently_active: Some(false),
displayname: None,
last_active_ago: Some(UInt::try_from(2_478_593).unwrap()),
presence: PresenceState::Online,
status_msg: Some("Making cupcakes".to_string()),
},
sender: UserId::try_from("@example:localhost").unwrap(),
};
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"
});
assert_matches!(
from_json_value::<EventJson<PresenceEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
PresenceEvent {
content: PresenceEventContent {
avatar_url: Some(avatar_url),
currently_active: Some(false),
displayname: None,
last_active_ago: Some(last_active_ago),
presence: PresenceState::Online,
status_msg: Some(status_msg),
},
sender,
} if avatar_url == "mxc://localhost:wefuiwegh8742w"
&& status_msg == "Making cupcakes"
&& sender == "@example:localhost"
&& last_active_ago == UInt::from(2_478_593u32)
);
}
}

View File

@ -0,0 +1,464 @@
//! Types for the the *m.push_rules* event.
use ruma_events_macros::BasicEventContent;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// Describes all push rules for a user.
pub type PushRulesEvent = BasicEvent<PushRulesEventContent>;
/// The payload for `PushRulesEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.push_rules")]
pub struct PushRulesEventContent {
/// The global ruleset.
pub global: Ruleset,
}
pub use ruma_common::push::Action;
/// 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, Deserialize, Serialize)]
pub struct Ruleset {
/// These rules configure behavior for (unencrypted) messages that match certain patterns.
pub content: Vec<PatternedPushRule>,
/// 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_: Vec<ConditionalPushRule>,
/// These rules change the behavior of all messages for a given room.
pub room: Vec<PushRule>,
/// These rules configure notification behavior for messages from a specific Matrix user ID.
pub sender: Vec<PushRule>,
/// These rules are identical to override rules, but have a lower priority than `content`,
/// `room` and `sender` rules.
pub underride: Vec<ConditionalPushRule>,
}
/// 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.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PushRule {
/// Actions to determine if and how a notification is delivered for events matching this rule.
pub actions: Vec<Action>,
/// 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,
}
/// Like `PushRule`, but with an additional `conditions` field.
///
/// Only applicable to underride and override rules.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConditionalPushRule {
/// Actions to determine if and how a notification is delivered for events matching this rule.
pub actions: Vec<Action>,
/// 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<PushCondition>,
}
/// Like `PushRule`, but with an additional `pattern` field.
///
/// Only applicable to content rules.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PatternedPushRule {
/// Actions to determine if and how a notification is delivered for events matching this rule.
pub actions: Vec<Action>,
/// 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,
}
/// A condition that must apply for an associated push rule's action to be taken.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PushCondition {
/// This is 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,
},
/// This matches unencrypted messages where `content.body` contains the owner's display name in
/// that room.
ContainsDisplayName,
/// This matches the current number of members in the room.
RoomMemberCount {
/// 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 `==`.
is: String,
},
/// This 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,
},
}
#[cfg(test)]
mod tests {
use matches::assert_matches;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{PushCondition, PushRulesEvent};
use crate::EventJson;
#[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".to_string(),
pattern: "m.notice".to_string(),
})
.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: "2".to_string(),
})
.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".to_string(),
})
.unwrap()
);
}
#[test]
fn deserialize_event_match_condition() {
let json_data = json!({
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
});
assert_matches!(
from_json_value::<PushCondition>(json_data).unwrap(),
PushCondition::EventMatch { key, pattern }
if key == "content.msgtype" && pattern == "m.notice"
);
}
#[test]
fn deserialize_contains_display_name_condition() {
assert_matches!(
from_json_value::<PushCondition>(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"
});
assert_matches!(
from_json_value::<PushCondition>(json_data).unwrap(),
PushCondition::RoomMemberCount { is }
if is == "2"
);
}
#[test]
fn deserialize_sender_notification_permission_condition() {
let json_data = json!({
"key": "room",
"kind": "sender_notification_permission"
});
assert_matches!(
from_json_value::<PushCondition>(json_data).unwrap(),
PushCondition::SenderNotificationPermission {
key
} if key == "room"
);
}
#[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"
});
let _ = from_json_value::<EventJson<PushRulesEvent>>(json_data)
.unwrap()
.deserialize()
.unwrap();
}
}

View File

@ -0,0 +1,63 @@
//! Types for the *m.receipt* event.
use std::{
collections::BTreeMap,
ops::{Deref, DerefMut},
time::SystemTime,
};
use ruma_events_macros::EphemeralRoomEventContent;
use ruma_identifiers::{EventId, UserId};
use serde::{Deserialize, Serialize};
use crate::EphemeralRoomEvent;
/// Informs the client who has read a message specified by it's event id.
pub type ReceiptEvent = EphemeralRoomEvent<ReceiptEventContent>;
/// The payload for `ReceiptEvent`.
///
/// 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, EphemeralRoomEventContent)]
#[ruma_event(type = "m.receipt")]
pub struct ReceiptEventContent(pub BTreeMap<EventId, Receipts>);
impl Deref for ReceiptEventContent {
type Target = BTreeMap<EventId, Receipts>;
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.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Receipts {
/// A collection of users who have sent *m.read* receipts for this event.
#[serde(default, rename = "m.read")]
pub read: Option<UserReceipts>,
}
/// A mapping of user ID to receipt.
///
/// The user ID is the entity who sent this receipt.
pub type UserReceipts = BTreeMap<UserId, Receipt>;
/// An acknowledgement of an event.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Receipt {
/// The time when the receipt was sent.
#[serde(
with = "ruma_serde::time::opt_ms_since_unix_epoch",
default,
skip_serializing_if = "Option::is_none"
)]
pub ts: Option<SystemTime>,
}

120
ruma-events/src/room.rs Normal file
View File

@ -0,0 +1,120 @@
//! Modules for events in the *m.room* namespace.
//!
//! This module also contains types shared by events in its child namespaces.
use std::collections::BTreeMap;
use js_int::UInt;
use serde::{Deserialize, Serialize};
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;
pub mod tombstone;
pub mod topic;
/// Metadata about an image.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ImageInfo {
/// The height of the image in pixels.
#[serde(rename = "h", skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
/// The width of the image in pixels.
#[serde(rename = "w", skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
/// The MIME type of the image, e.g. "image/png."
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The file size of the image in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL to the thumbnail of the image. Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
/// Information on the encrypted thumbnail image. Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
}
/// Metadata about a thumbnail.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ThumbnailInfo {
/// The height of the thumbnail in pixels.
#[serde(rename = "h", skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
/// The width of the thumbnail in pixels.
#[serde(rename = "w", skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
/// The MIME type of the thumbnail, e.g. "image/png."
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The file size of the thumbnail in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
}
/// A file sent to a room with end-to-end encryption enabled.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EncryptedFile {
/// The URL to the file.
pub url: String,
/// 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: String,
/// 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<String, String>,
/// Version of the encrypted attachments protocol. Must be `v2`.
pub v: String,
}
/// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct JsonWebKey {
/// Key type. Must be `oct`.
pub kty: String,
/// Key operations. Must at least contain `encrypt` and `decrypt`.
pub key_ops: Vec<String>,
/// Required. Algorithm. Must be `A256CTR`.
pub alg: String,
/// The key, encoded as urlsafe unpadded base64.
pub k: String,
/// Extractable. Must be `true`. This is a
/// [W3C extension](https://w3c.github.io/webcrypto/#iana-section-jwk).
pub ext: bool,
}

View File

@ -0,0 +1,18 @@
//! Types for the *m.room.aliases* event.
use ruma_events_macros::StateEventContent;
use ruma_identifiers::RoomAliasId;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// Informs the room about what room aliases it has been given.
pub type AliasesEvent = StateEvent<AliasesEventContent>;
/// The payload for `AliasesEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.aliases")]
pub struct AliasesEventContent {
/// A list of room aliases.
pub aliases: Vec<RoomAliasId>,
}

View File

@ -0,0 +1,25 @@
//! Types for the *m.room.avatar* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use super::ImageInfo;
use crate::StateEvent;
/// A picture that is associated with the room.
///
/// This can be displayed alongside the room information.
pub type AvatarEvent = StateEvent<AvatarEventContent>;
/// The payload for `AvatarEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.avatar")]
pub struct AvatarEventContent {
/// Information about the avatar image.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<ImageInfo>>,
/// Information about the avatar thumbnail image.
/// URL of the avatar image.
pub url: String,
}

View File

@ -0,0 +1,172 @@
//! Types for the *m.room.canonical_alias* event.
use ruma_events_macros::StateEventContent;
use ruma_identifiers::RoomAliasId;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// Informs the room as to which alias is the canonical one.
pub type CanonicalAliasEvent = StateEvent<CanonicalAliasEventContent>;
/// The payload for `CanonicalAliasEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.canonical_alias")]
pub struct CanonicalAliasEventContent {
/// The canonical alias.
///
/// Rooms with `alias: None` should be treated the same as a room
/// with no canonical alias.
#[serde(
default,
deserialize_with = "ruma_serde::empty_string_as_none",
skip_serializing_if = "Option::is_none"
)]
pub alias: Option<RoomAliasId>,
/// List of alternative aliases to the room.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub alt_aliases: Vec<RoomAliasId>,
}
#[cfg(test)]
mod tests {
use std::{
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::CanonicalAliasEventContent;
use crate::{EventJson, StateEvent, UnsignedData};
#[test]
fn serialization_with_optional_fields_as_none() {
let canonical_alias_event = StateEvent {
content: CanonicalAliasEventContent {
alias: Some(RoomAliasId::try_from("#somewhere:localhost").unwrap()),
alt_aliases: Vec::new(),
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: None,
room_id: RoomId::try_from("!dummy:example.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "".to_string(),
unsigned: UnsignedData::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::<EventJson<StateEvent<CanonicalAliasEventContent>>>(json_data)
.unwrap()
.deserialize()
.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::<EventJson<StateEvent<CanonicalAliasEventContent>>>(json_data)
.unwrap()
.deserialize()
.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::<EventJson<StateEvent<CanonicalAliasEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap()
.content
.alias,
None
);
}
#[test]
fn nonempty_field_as_some() {
let alias = Some(RoomAliasId::try_from("#somewhere:localhost").unwrap());
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::<EventJson<StateEvent<CanonicalAliasEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap()
.content
.alias,
alias
);
}
}

View File

@ -0,0 +1,105 @@
//! Types for the *m.room.create* event.
use std::convert::TryFrom;
use ruma_events_macros::StateEventContent;
use ruma_identifiers::{EventId, RoomId, RoomVersionId, UserId};
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// This is the first event in a room and cannot be changed. It acts as the root of all other
/// events.
pub type CreateEvent = StateEvent<CreateEventContent>;
/// The payload for `CreateEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.create")]
pub struct CreateEventContent {
/// The `user_id` of the room creator. This is set by the homeserver.
pub creator: UserId,
/// Whether or not this room's data should be transferred to other homeservers.
#[serde(
rename = "m.federate",
default = "ruma_serde::default_true",
skip_serializing_if = "ruma_serde::is_true"
)]
pub federate: bool,
/// The version of the room. Defaults to "1" if the key does not exist.
#[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<PreviousRoom>,
}
/// A reference to an old room replaced during a room version upgrade.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PreviousRoom {
/// The ID of the old room.
pub room_id: RoomId,
/// The event ID of the last known event in the old room.
pub event_id: EventId,
}
/// Used to default the `room_version` field to room version 1.
fn default_room_version_id() -> RoomVersionId {
RoomVersionId::try_from("1").unwrap()
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use matches::assert_matches;
use ruma_identifiers::{RoomVersionId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::CreateEventContent;
use crate::EventJson;
#[test]
fn serialization() {
let content = CreateEventContent {
creator: UserId::try_from("@carl:example.com").unwrap(),
federate: false,
room_version: RoomVersionId::version_4(),
predecessor: None,
};
let json = json!({
"creator": "@carl:example.com",
"m.federate": false,
"room_version": "4"
});
assert_eq!(to_json_value(&content).unwrap(), json);
}
#[test]
fn deserialization() {
let json = json!({
"creator": "@carl:example.com",
"m.federate": true,
"room_version": "4"
});
assert_matches!(
from_json_value::<EventJson<CreateEventContent>>(json)
.unwrap()
.deserialize()
.unwrap(),
CreateEventContent {
creator,
federate: true,
room_version,
predecessor: None,
} if creator == "@carl:example.com"
&& room_version == RoomVersionId::version_4()
);
}
}

View File

@ -0,0 +1,165 @@
//! Types for the *m.room.encrypted* event.
use std::collections::BTreeMap;
use js_int::UInt;
use ruma_events_macros::StateEventContent;
use ruma_identifiers::DeviceId;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// An event that defines how messages sent in this room should be encrypted.
pub type EncryptedEvent = StateEvent<EncryptedEventContent>;
/// The payload for `EncryptedEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[non_exhaustive]
#[ruma_event(type = "m.room.encrypted")]
#[serde(tag = "algorithm")]
pub enum EncryptedEventContent {
/// 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),
}
/// The payload for `EncryptedEvent` using the *m.olm.v1.curve25519-aes-sha2* algorithm.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OlmV1Curve25519AesSha2Content {
/// A map from the recipient Curve25519 identity key to ciphertext information.
pub ciphertext: BTreeMap<String, CiphertextInfo>,
/// The Curve25519 key of the sender.
pub sender_key: String,
}
/// 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)]
pub struct CiphertextInfo {
/// The encrypted payload.
pub body: String,
/// The Olm message type.
#[serde(rename = "type")]
pub message_type: UInt,
}
/// The payload for `EncryptedEvent` using the *m.megolm.v1.aes-sha2* algorithm.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MegolmV1AesSha2Content {
/// 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: DeviceId,
/// The ID of the session used to encrypt the message.
pub session_id: String,
}
#[cfg(test)]
mod tests {
use matches::assert_matches;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{EncryptedEventContent, MegolmV1AesSha2Content};
use crate::EventJson;
#[test]
fn serialization() {
let key_verification_start_content =
EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content {
ciphertext: "ciphertext".to_string(),
sender_key: "sender_key".to_string(),
device_id: "device_id".to_string(),
session_id: "session_id".to_string(),
});
let json_data = json!({
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ciphertext",
"sender_key": "sender_key",
"device_id": "device_id",
"session_id": "session_id"
});
assert_eq!(
to_json_value(&key_verification_start_content).unwrap(),
json_data
);
}
#[test]
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"
});
assert_matches!(
from_json_value::<EventJson<EncryptedEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content {
ciphertext,
sender_key,
device_id,
session_id,
}) if ciphertext == "ciphertext"
&& sender_key == "sender_key"
&& device_id == "device_id"
&& session_id == "session_id"
);
}
#[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 = from_json_value::<EventJson<EncryptedEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap();
match content {
EncryptedEventContent::OlmV1Curve25519AesSha2(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, 1u16.into());
}
_ => panic!("Wrong content type, expected a OlmV1 content"),
}
}
#[test]
fn deserialization_failure() {
assert!(from_json_value::<EventJson<EncryptedEventContent>>(
json!({ "algorithm": "m.megolm.v1.aes-sha2" })
)
.unwrap()
.deserialize()
.is_err());
}
}

View File

@ -0,0 +1,30 @@
//! Types for the *m.room.encryption* event.
use js_int::UInt;
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use crate::{Algorithm, StateEvent};
/// Defines how messages sent in this room should be encrypted.
pub type EncryptionEvent = StateEvent<EncryptionEventContent>;
/// The payload for `EncryptionEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.encryption")]
pub struct EncryptionEventContent {
/// The encryption algorithm to be used to encrypt messages sent in this room.
///
/// Must be `m.megolm.v1.aes-sha2`.
pub algorithm: Algorithm,
/// How long the session should be used before changing it.
///
/// 604800000 (a week) is the recommended default.
pub rotation_period_ms: Option<UInt>,
/// How many messages should be sent before changing the session.
///
/// 100 is the recommended default.
pub rotation_period_msgs: Option<UInt>,
}

View File

@ -0,0 +1,34 @@
//! Types for the *m.room.guest_access* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::StateEvent;
/// 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`.
pub type GuestAccessEvent = StateEvent<GuestAccessEventContent>;
/// The payload for `GuestAccessEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.guest_access")]
pub struct GuestAccessEventContent {
/// A policy for guest user access to a room.
pub guest_access: GuestAccess,
}
/// A policy for guest user access to a room.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum GuestAccess {
/// Guests are allowed to join the room.
CanJoin,
/// Guests are not allowed to join the room.
Forbidden,
}

View File

@ -0,0 +1,43 @@
//! Types for the *m.room.history_visibility* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::StateEvent;
/// This event controls whether a member of a room can see the events that happened in a room
/// from before they joined.
pub type HistoryVisibilityEvent = StateEvent<HistoryVisibilityEventContent>;
/// The payload for `HistoryVisibilityEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.history_visibility")]
pub struct HistoryVisibilityEventContent {
/// Who can see the room history.
pub history_visibility: HistoryVisibility,
}
/// Who can see a room's history.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
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,
}

View File

@ -0,0 +1,37 @@
//! Types for the *m.room.join_rules* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::StateEvent;
/// Describes how users are allowed to join the room.
pub type JoinRulesEvent = StateEvent<JoinRulesEventContent>;
/// The payload for `JoinRulesEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.join_rules")]
pub struct JoinRulesEventContent {
/// The type of rules used for users wishing to join this room.
pub join_rule: JoinRule,
}
/// The rule used for users wishing to join this room.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
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,
/// Reserved but not yet implemented by the Matrix specification.
Knock,
/// Reserved but not yet implemented by the Matrix specification.
Private,
/// Anyone can join the room without any prior action.
Public,
}

View File

@ -0,0 +1,468 @@
//! Types for the *m.room.member* event.
use std::collections::BTreeMap;
use ruma_events_macros::StateEventContent;
use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::StateEvent;
/// 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/<room id>/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.
///
/// The `third_party_invite` property will be set if this invite is an *invite* event and is the
/// successor of an *m.room.third_party_invite* event, and absent otherwise.
///
/// This event may also include an `invite_room_state` key inside the event's unsigned data. If
/// present, this contains an array of `StrippedState` events. These events provide information
/// on a subset of state events such as the room name. Note that ruma-events treats unsigned
/// data on events as arbitrary JSON values, and the ruma-events types for this event don't
/// provide direct access to `invite_room_state`. If you need this data, you must extract and
/// convert it from a `serde_json::Value` yourself.
///
/// 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.
pub type MemberEvent = StateEvent<MemberEventContent>;
/// The payload for `MemberEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.member")]
pub struct MemberEventContent {
/// The avatar URL for this user, if any. This is added by the homeserver.
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
/// The display name for this user, if any. This is added by the homeserver.
#[serde(skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
/// Flag indicating if 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<bool>,
/// 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<ThirdPartyInvite>,
}
/// The membership state of a user.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
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,
}
/// Information about a third party invitation.
#[derive(Clone, Debug, Deserialize, Serialize)]
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,
}
/// A block of content which has been signed, which servers can use to verify a third party
/// invitation.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SignedContent {
/// The invited Matrix user ID.
///
/// Must be equal to the user_id property of the event.
pub mxid: UserId,
/// A single signature from the verifying server, in the format specified by the Signing Events
/// section of the server-server API.
pub signatures: BTreeMap<String, BTreeMap<String, String>>,
/// The token property of the containing third_party_invite object.
pub token: String,
}
/// Translation of the membership change in `m.room.member` event.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum MembershipChange {
/// 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,
/// `displayname` or `avatar_url` changed.
ProfileChanged {
/// Whether the `displayname` changed.
displayname_changed: bool,
/// Whether the `avatar_url` changed.
avatar_url_changed: bool,
},
/// Not implemented.
NotImplemented,
}
impl MemberEvent {
/// Helper function for membership change. Check [the specification][spec] for details.
///
/// [spec]: https://matrix.org/docs/spec/client_server/latest#m-room-member
pub fn membership_change(&self) -> MembershipChange {
use MembershipState::*;
let prev_content = if let Some(prev_content) = &self.prev_content {
prev_content
} else {
&MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: Leave,
third_party_invite: None,
}
};
match (prev_content.membership, &self.content.membership) {
(Invite, Invite) | (Leave, Leave) | (Ban, Ban) => MembershipChange::None,
(Invite, Join) | (Leave, Join) => MembershipChange::Joined,
(Invite, Leave) => {
if self.sender == self.state_key {
MembershipChange::InvitationRevoked
} else {
MembershipChange::InvitationRejected
}
}
(Invite, Ban) | (Leave, Ban) => MembershipChange::Banned,
(Join, Invite) | (Ban, Invite) | (Ban, Join) => MembershipChange::Error,
(Join, Join) => MembershipChange::ProfileChanged {
displayname_changed: prev_content.displayname != self.content.displayname,
avatar_url_changed: prev_content.avatar_url != self.content.avatar_url,
},
(Join, Leave) => {
if self.sender == self.state_key {
MembershipChange::Left
} else {
MembershipChange::Kicked
}
}
(Join, Ban) => MembershipChange::KickedAndBanned,
(Leave, Invite) => MembershipChange::Invited,
(Ban, Leave) => MembershipChange::Unbanned,
(Knock, _) | (_, Knock) => MembershipChange::NotImplemented,
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, UNIX_EPOCH};
use maplit::btreemap;
use matches::assert_matches;
use serde_json::{from_value as from_json_value, json};
use super::{MemberEventContent, MembershipState, SignedContent, ThirdPartyInvite};
use crate::{EventJson, StateEvent};
#[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": "example.com"
});
assert_matches!(
from_json_value::<EventJson<StateEvent<MemberEventContent>>>(json)
.unwrap()
.deserialize()
.unwrap(),
StateEvent::<MemberEventContent> {
content: MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
},
event_id,
origin_server_ts,
room_id,
sender,
state_key,
unsigned,
prev_content: None,
} if event_id == "$h29iv0s8:example.com"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == "!n8f893n9:example.com"
&& sender == "@carl:example.com"
&& state_key == "example.com"
&& unsigned.is_empty()
);
}
#[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,
"prev_content": {
"membership": "join"
},
"room_id": "!n8f893n9:example.com",
"sender": "@carl:example.com",
"state_key": "example.com"
});
assert_matches!(
from_json_value::<EventJson<StateEvent<MemberEventContent>>>(json)
.unwrap()
.deserialize()
.unwrap(),
StateEvent::<MemberEventContent> {
content: MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
},
event_id,
origin_server_ts,
room_id,
sender,
state_key,
unsigned,
prev_content: Some(MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
}),
} if event_id == "$h29iv0s8:example.com"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == "!n8f893n9:example.com"
&& sender == "@carl:example.com"
&& state_key == "example.com"
&& unsigned.is_empty()
);
}
#[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"
});
assert_matches!(
from_json_value::<EventJson<StateEvent<MemberEventContent>>>(json)
.unwrap()
.deserialize()
.unwrap(),
StateEvent::<MemberEventContent> {
content: MemberEventContent {
avatar_url: Some(avatar_url),
displayname: Some(displayname),
is_direct: Some(true),
membership: MembershipState::Invite,
third_party_invite: Some(ThirdPartyInvite {
display_name: third_party_displayname,
signed: SignedContent { mxid, signatures, token },
}),
},
event_id,
origin_server_ts,
room_id,
sender,
state_key,
unsigned,
prev_content: None,
} if avatar_url == "mxc://example.org/SEsfnsuifSDFSSEF"
&& displayname == "Alice Margatroid"
&& third_party_displayname == "alice"
&& mxid == "@alice:example.org"
&& signatures == btreemap! {
"magic.forest".to_owned() => btreemap! {
"ed25519:3".to_owned() => "foobar".to_owned()
}
}
&& token == "abc123"
&& event_id == "$143273582443PhrSn:example.org"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(233)
&& room_id == "!jEsUZKDJdhlrceRyVU:example.org"
&& sender == "@alice:example.org"
&& state_key == "@alice:example.org"
&& unsigned.is_empty()
)
}
#[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,
"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"
}
}
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"state_key": "@alice:example.org"
});
assert_matches!(
from_json_value::<EventJson<StateEvent<MemberEventContent>>>(json)
.unwrap()
.deserialize()
.unwrap(),
StateEvent::<MemberEventContent> {
content: MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
},
event_id,
origin_server_ts,
room_id,
sender,
state_key,
unsigned,
prev_content: Some(MemberEventContent {
avatar_url: Some(avatar_url),
displayname: Some(displayname),
is_direct: Some(true),
membership: MembershipState::Invite,
third_party_invite: Some(ThirdPartyInvite {
display_name: third_party_displayname,
signed: SignedContent { mxid, signatures, token },
}),
}),
} if event_id == "$143273582443PhrSn:example.org"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(233)
&& room_id == "!jEsUZKDJdhlrceRyVU:example.org"
&& sender == "@alice:example.org"
&& state_key == "@alice:example.org"
&& unsigned.is_empty()
&& avatar_url == "mxc://example.org/SEsfnsuifSDFSSEF"
&& displayname == "Alice Margatroid"
&& third_party_displayname == "alice"
&& mxid == "@alice:example.org"
&& signatures == btreemap! {
"magic.forest".to_owned() => btreemap! {
"ed25519:3".to_owned() => "foobar".to_owned()
}
}
&& token == "abc123"
);
}
}

View File

@ -0,0 +1,559 @@
//! Types for the *m.room.message* event.
use js_int::UInt;
use ruma_events_macros::MessageEventContent;
use ruma_identifiers::EventId;
use serde::{Deserialize, Serialize};
use super::{EncryptedFile, ImageInfo, ThumbnailInfo};
pub mod feedback;
use crate::MessageEvent as OuterMessageEvent;
/// This event is used when sending messages in a room.
///
/// Messages are not limited to be text.
pub type MessageEvent = OuterMessageEvent<MessageEventContent>;
/// The payload for `MessageEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.room.message")]
#[serde(tag = "msgtype")]
pub enum MessageEventContent {
/// An audio message.
#[serde(rename = "m.audio")]
Audio(AudioMessageEventContent),
/// An emote message.
#[serde(rename = "m.emote")]
Emote(EmoteMessageEventContent),
/// A file message.
#[serde(rename = "m.file")]
File(FileMessageEventContent),
/// An image message.
#[serde(rename = "m.image")]
Image(ImageMessageEventContent),
/// A location message.
#[serde(rename = "m.location")]
Location(LocationMessageEventContent),
/// A notice message.
#[serde(rename = "m.notice")]
Notice(NoticeMessageEventContent),
/// A server notice message.
#[serde(rename = "m.server_notice")]
ServerNotice(ServerNoticeMessageEventContent),
/// A text message.
#[serde(rename = "m.text")]
Text(TextMessageEventContent),
/// A video message.
#[serde(rename = "m.video")]
Video(VideoMessageEventContent),
}
/// The payload for an audio message.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AudioMessageEventContent {
/// The textual representation of this message.
pub body: String,
/// Metadata for the audio clip referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<AudioInfo>>,
/// The URL to the audio clip. Required if the file is unencrypted. The URL (typically
/// [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.1#mxc-uri)) to the audio clip.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Required if the audio clip is encrypted. Information on the encrypted audio clip.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
}
/// Metadata about an audio clip.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AudioInfo {
/// The duration of the audio in milliseconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
/// The mimetype of the audio, e.g. "audio/aac."
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The size of the audio clip in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
}
/// The payload for an emote message.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmoteMessageEventContent {
/// The emote action to perform.
pub body: String,
/// Formatted form of the message `body`.
#[serde(flatten)]
pub formatted: Option<FormattedBody>,
}
/// The payload for a file message.
#[derive(Clone, Debug, Deserialize, Serialize)]
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<String>,
/// Metadata about the file referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<FileInfo>>,
/// The URL to the file. Required if the file is unencrypted. The URL (typically
/// [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.1#mxc-uri)) to the file.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Required if file is encrypted. Information on the encrypted file.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
}
/// Metadata about a file.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FileInfo {
/// The mimetype of the file, e.g. "application/msword."
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The size of the file in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
/// Information on the encrypted thumbnail file. Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
}
/// The payload for an image message.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ImageMessageEventContent {
/// A textual representation of the image. This 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,
/// Metadata about the image referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<ImageInfo>>,
/// The URL to the image. Required if the file is unencrypted. The URL (typically
/// [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.1#mxc-uri)) to the image.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Required if image is encrypted. Information on the encrypted image.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
}
/// The payload for a location message.
#[derive(Clone, Debug, Deserialize, Serialize)]
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<Box<LocationInfo>>,
}
/// Thumbnail info associated with a location.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LocationInfo {
/// Metadata about the image referred to in `thumbnail_url` or `thumbnail_file`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL to a thumbnail of the location being represented. Only present if the thumbnail is
/// unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
/// Information on an encrypted thumbnail of the location being represented. Only present if the
/// thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
}
/// The payload for a notice message.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NoticeMessageEventContent {
/// The notice text to send.
pub body: String,
/// Formatted form of the message `body`.
#[serde(flatten)]
pub formatted: Option<FormattedBody>,
/// Information about related messages for
/// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies).
#[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
pub relates_to: Option<RelatesTo>,
}
/// The payload for a server notice message.
#[derive(Clone, Debug, Deserialize, Serialize)]
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<String>,
/// 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<LimitType>,
}
/// Types of server notices.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum ServerNoticeType {
/// The server has exceeded some limit which requires the server administrator to intervene.
#[serde(rename = "m.server_notice.usage_limit_reached")]
UsageLimitReached,
}
/// Types of usage limits.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
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,
}
/// The format for the formatted representation of a message body.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum MessageFormat {
/// HTML.
#[serde(rename = "org.matrix.custom.html")]
Html,
/// A custom message format.
Custom(String),
}
/// Common message event content fields for message types that have separate plain-text and
/// formatted representations.
#[derive(Clone, Debug, Deserialize, Serialize)]
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,
}
/// The payload for a text message.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TextMessageEventContent {
/// The body of the message.
pub body: String,
/// Formatted form of the message `body`.
#[serde(flatten)]
pub formatted: Option<FormattedBody>,
/// Information about related messages for
/// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies).
#[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
pub relates_to: Option<RelatesTo>,
}
/// The payload for a video message.
#[derive(Clone, Debug, Deserialize, Serialize)]
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,
/// Metadata about the video clip referred to in `url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<VideoInfo>>,
/// The URL to the video clip. Required if the file is unencrypted. The URL (typically
/// [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.1#mxc-uri)) to the video clip.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Required if video clip is encrypted. Information on the encrypted video clip.
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
}
/// Metadata about a video.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct VideoInfo {
/// The duration of the video in milliseconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
/// The height of the video in pixels.
#[serde(rename = "h")]
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
/// The width of the video in pixels.
#[serde(rename = "w")]
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
/// The mimetype of the video, e.g. "video/mp4."
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The size of the video in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about an image.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// The URL (typically [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.1#mxc-uri)) to
/// an image thumbnail of the video clip. Only present if the thumbnail is unencrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
/// Information on the encrypted thumbnail file. Only present if the thumbnail is encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
}
/// Information about related messages for
/// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies).
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RelatesTo {
/// Information about another message being replied to.
#[serde(rename = "m.in_reply_to")]
pub in_reply_to: InReplyTo,
}
/// Information about the event a "rich reply" is replying to.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InReplyTo {
/// The event being replied to.
pub event_id: EventId,
}
impl TextMessageEventContent {
/// A convenience constructor to create a plain text message
pub fn new_plain(body: impl Into<String>) -> TextMessageEventContent {
TextMessageEventContent {
body: body.into(),
formatted: None,
relates_to: None,
}
}
}
#[cfg(test)]
mod tests {
use std::{
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use matches::assert_matches;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{AudioMessageEventContent, FormattedBody, MessageEventContent, MessageFormat};
use crate::{
room::message::{InReplyTo, RelatesTo, TextMessageEventContent},
EventJson, MessageEvent, UnsignedData,
};
#[test]
fn serialization() {
let ev = MessageEvent {
content: MessageEventContent::Audio(AudioMessageEventContent {
body: "test".to_string(),
info: None,
url: Some("http://example.com/audio.mp3".to_string()),
file: None,
}),
event_id: EventId::try_from("$143273582443PhrSn:example.org").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(10_000),
room_id: RoomId::try_from("!testroomid:example.org").unwrap(),
sender: UserId::try_from("@user:example.org").unwrap(),
unsigned: UnsignedData::default(),
};
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": "http://example.com/audio.mp3",
}
})
);
}
#[test]
fn content_serialization() {
let message_event_content = MessageEventContent::Audio(AudioMessageEventContent {
body: "test".to_string(),
info: None,
url: Some("http://example.com/audio.mp3".to_string()),
file: None,
});
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "test",
"msgtype": "m.audio",
"url": "http://example.com/audio.mp3"
})
);
}
#[test]
fn formatted_body_serialization() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent {
body: "Hello, World!".into(),
formatted: Some(FormattedBody {
format: MessageFormat::Html,
body: "Hello, <em>World</em>!".into(),
}),
relates_to: None,
});
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "Hello, World!",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "Hello, <em>World</em>!",
})
);
}
#[test]
fn plain_text_content_serialization() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent::new_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.text"
})
);
}
#[test]
fn relates_to_content_serialization() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent {
body: "> <@test:example.com> test\n\ntest reply".to_owned(),
formatted: None,
relates_to: Some(RelatesTo {
in_reply_to: InReplyTo {
event_id: EventId::try_from("$15827405538098VGFWH:example.com").unwrap(),
},
}),
});
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]
fn content_deserialization() {
let json_data = json!({
"body": "test",
"msgtype": "m.audio",
"url": "http://example.com/audio.mp3"
});
assert_matches!(
from_json_value::<EventJson<MessageEventContent>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
MessageEventContent::Audio(AudioMessageEventContent {
body,
info: None,
url: Some(url),
file: None,
}) if body == "test" && url == "http://example.com/audio.mp3"
);
}
#[test]
fn content_deserialization_failure() {
let json_data = json!({
"body": "test","msgtype": "m.location",
"url": "http://example.com/audio.mp3"
});
assert!(from_json_value::<EventJson<MessageEventContent>>(json_data)
.unwrap()
.deserialize()
.is_err());
}
}

View File

@ -0,0 +1,38 @@
//! Types for the *m.room.message.feedback* event.
use ruma_events_macros::MessageEventContent;
use ruma_identifiers::EventId;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::MessageEvent;
/// An acknowledgement of a message.
///
/// N.B.: Usage of this event is discouraged in favor of the receipts module. Most clients will
/// not recognize this event.
pub type FeedbackEvent = MessageEvent<FeedbackEventContent>;
/// The payload for `FeedbackEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.room.message.feedback")]
pub struct FeedbackEventContent {
/// The event that this feedback is related to.
pub target_event_id: EventId,
/// The type of feedback.
#[serde(rename = "type")]
pub feedback_type: FeedbackType,
}
/// A type of feedback.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FeedbackType {
/// Sent when a message is received.
Delivered,
/// Sent when a message has been observed by the end user.
Read,
}

View File

@ -0,0 +1,275 @@
//! Types for the *m.room.name* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use crate::{InvalidInput, StateEvent};
/// The room name is a human-friendly string designed to be displayed to the end-user.
pub type NameEvent = StateEvent<NameEventContent>;
/// The payload for `NameEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.name")]
pub struct NameEventContent {
/// The name of the room. This MUST NOT exceed 255 bytes.
#[serde(default, deserialize_with = "room_name")]
pub(crate) name: Option<String>,
}
impl NameEventContent {
/// Create a new `NameEventContent` with the given name.
///
/// # Errors
///
/// `InvalidInput` will be returned if the name is more than 255 bytes.
pub fn new(name: String) -> Result<Self, InvalidInput> {
match name.len() {
0 => Ok(Self { name: None }),
1..=255 => Ok(Self { name: Some(name) }),
_ => Err(InvalidInput(
"a room name cannot be more than 255 bytes".to_string(),
)),
}
}
/// The name of the room, if any.
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
}
fn room_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
use serde::de::Error;
// this handles the null case and the empty string or nothing case
match Option::<String>::deserialize(deserializer)? {
Some(name) => match name.len() {
0 => Ok(None),
1..=255 => Ok(Some(name)),
_ => Err(D::Error::custom(
"a room name cannot be more than 255 bytes",
)),
},
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use std::{
convert::TryFrom,
iter::FromIterator,
time::{Duration, UNIX_EPOCH},
};
use js_int::Int;
use matches::assert_matches;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use crate::{EventJson, StateEvent, UnsignedData};
use super::NameEventContent;
#[test]
fn serialization_with_optional_fields_as_none() {
let name_event = StateEvent {
content: NameEventContent {
name: Some("The room name".to_string()),
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: None,
room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "".to_string(),
unsigned: UnsignedData::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 = StateEvent {
content: NameEventContent {
name: Some("The room name".to_string()),
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: Some(NameEventContent {
name: Some("The old name".to_string()),
}),
room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "".to_string(),
unsigned: UnsignedData {
age: Some(Int::from(100)),
..UnsignedData::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,
"prev_content": { "name": "The old name" },
"room_id": "!n8f893n9:example.com",
"sender": "@carl:example.com",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 100
}
});
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::<EventJson<StateEvent<NameEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap()
.content
.name,
None
);
}
#[test]
fn name_fails_validation_when_too_long() {
// "XXXX .." 256 times
let long_string: String = String::from_iter(std::iter::repeat('X').take(256));
assert_eq!(long_string.len(), 256);
let long_content_json = json!({ "name": &long_string });
let from_raw: EventJson<NameEventContent> = from_json_value(long_content_json).unwrap();
let result = from_raw.deserialize();
assert!(result.is_err(), "Result should be invalid: {:?}", result);
}
#[test]
fn json_with_empty_name_creates_content_as_none() {
let long_content_json = json!({ "name": "" });
let from_raw: EventJson<NameEventContent> = from_json_value(long_content_json).unwrap();
assert_matches!(
from_raw.deserialize().unwrap(),
NameEventContent { name: None }
);
}
#[test]
fn new_with_empty_name_creates_content_as_none() {
assert_matches!(
NameEventContent::new(String::new()).unwrap(),
NameEventContent { 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::<EventJson<StateEvent<NameEventContent>>>(json_data)
.unwrap()
.deserialize()
.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::<EventJson<StateEvent<NameEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap()
.content
.name,
None
);
}
#[test]
fn nonempty_field_as_some() {
let name = Some("The room name".to_string());
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::<EventJson<StateEvent<NameEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap()
.content
.name,
name
);
}
}

View File

@ -0,0 +1,66 @@
//! Types for the *m.room.pinned_events* event.
use ruma_events_macros::StateEventContent;
use ruma_identifiers::EventId;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// Used to "pin" particular events in a room for other participants to review later.
pub type PinnedEventsEvent = StateEvent<PinnedEventsEventContent>;
/// The payload for `PinnedEventsEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.pinned_events")]
pub struct PinnedEventsEventContent {
/// An ordered list of event IDs to pin.
pub pinned: Vec<EventId>,
}
#[cfg(test)]
mod tests {
use std::time::{Duration, UNIX_EPOCH};
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::to_string;
use super::PinnedEventsEventContent;
use crate::{EventJson, StateEvent, UnsignedData};
#[test]
fn serialization_deserialization() {
let mut content: PinnedEventsEventContent = PinnedEventsEventContent { pinned: Vec::new() };
content.pinned.push(EventId::new("example.com").unwrap());
content.pinned.push(EventId::new("example.com").unwrap());
let event = StateEvent {
content: content.clone(),
event_id: EventId::new("example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1_432_804_485_886u64),
prev_content: None,
room_id: RoomId::new("example.com").unwrap(),
sender: UserId::new("example.com").unwrap(),
state_key: "".to_string(),
unsigned: UnsignedData::default(),
};
let serialized_event = to_string(&event).unwrap();
let parsed_event = serde_json::from_str::<EventJson<StateEvent<PinnedEventsEventContent>>>(
&serialized_event,
)
.unwrap()
.deserialize()
.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]);
}
}

View File

@ -0,0 +1,286 @@
//! Types for the *m.room.power_levels* event.
use std::collections::BTreeMap;
use js_int::Int;
use ruma_events_macros::StateEventContent;
use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
use crate::{EventType, StateEvent};
/// Defines the power levels (privileges) of users in the room.
pub type PowerLevelsEvent = StateEvent<PowerLevelsEventContent>;
/// The payload for `PowerLevelsEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.power_levels")]
pub struct PowerLevelsEventContent {
/// The level required to ban a user.
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level"
)]
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")]
pub events: BTreeMap<EventType, Int>,
/// The default level required to send message events.
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
pub events_default: Int,
/// The level required to invite a user.
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level"
)]
pub invite: Int,
/// The level required to kick a user.
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level"
)]
pub kick: Int,
/// The level required to redact an event.
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level"
)]
pub redact: Int,
/// The default level required to send state events.
#[serde(
default = "default_power_level",
skip_serializing_if = "is_default_power_level"
)]
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")]
pub users: BTreeMap<UserId, Int>,
/// The default power level for every user in the room.
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
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 = "ruma_serde::is_default")]
pub notifications: NotificationPowerLevels,
}
impl Default for PowerLevelsEventContent {
fn default() -> Self {
// events_default and users_default 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::default(),
invite: default_power_level(),
kick: default_power_level(),
redact: default_power_level(),
state_default: default_power_level(),
users: BTreeMap::new(),
users_default: Int::default(),
notifications: NotificationPowerLevels::default(),
}
}
}
/// The power level requirements for specific notification types.
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub struct NotificationPowerLevels {
/// The level required to trigger an `@room` notification.
#[serde(default = "default_power_level")]
pub room: Int,
}
impl Default for NotificationPowerLevels {
fn default() -> Self {
Self {
room: default_power_level(),
}
}
}
/// Used to default power levels to 50 during deserialization.
fn default_power_level() -> Int {
Int::from(50)
}
/// 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::from(50)
}
#[cfg(test)]
mod tests {
use std::{
collections::BTreeMap,
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use js_int::Int;
use maplit::btreemap;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::{json, to_value as to_json_value};
use super::{default_power_level, NotificationPowerLevels, PowerLevelsEventContent};
use crate::{EventType, StateEvent, UnsignedData};
#[test]
fn serialization_with_optional_fields_as_none() {
let default = default_power_level();
let power_levels_event = StateEvent {
content: PowerLevelsEventContent {
ban: default,
events: BTreeMap::new(),
events_default: Int::from(0),
invite: default,
kick: default,
redact: default,
state_default: default,
users: BTreeMap::new(),
users_default: Int::from(0),
notifications: NotificationPowerLevels::default(),
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: None,
room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
unsigned: UnsignedData::default(),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "".to_string(),
};
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 = UserId::try_from("@carl:example.com").unwrap();
let power_levels_event = StateEvent {
content: PowerLevelsEventContent {
ban: Int::from(23),
events: btreemap! {
EventType::Dummy => Int::from(23)
},
events_default: Int::from(23),
invite: Int::from(23),
kick: Int::from(23),
redact: Int::from(23),
state_default: Int::from(23),
users: btreemap! {
user.clone() => Int::from(23)
},
users_default: Int::from(23),
notifications: NotificationPowerLevels {
room: Int::from(23),
},
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: Some(PowerLevelsEventContent {
// Make just one field different so we at least know they're two different objects.
ban: Int::from(42),
events: btreemap! {
EventType::Dummy => Int::from(42)
},
events_default: Int::from(42),
invite: Int::from(42),
kick: Int::from(42),
redact: Int::from(42),
state_default: Int::from(42),
users: btreemap! {
user.clone() => Int::from(42)
},
users_default: Int::from(42),
notifications: NotificationPowerLevels {
room: Int::from(42),
},
}),
room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
unsigned: UnsignedData {
age: Some(Int::from(100)),
..UnsignedData::default()
},
sender: user,
state_key: "".to_string(),
};
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,
"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
}
},
"room_id": "!n8f893n9:example.com",
"sender": "@carl:example.com",
"state_key": "",
"type": "m.room.power_levels",
"unsigned": {
"age": 100
}
});
assert_eq!(actual, expected);
}
}

View File

@ -0,0 +1,145 @@
//! Types for the *m.room.redaction* event.
use std::time::SystemTime;
use ruma_events_macros::{Event, EventContent};
use ruma_identifiers::{EventId, RoomId, UserId};
use serde::{Deserialize, Serialize};
use crate::UnsignedData;
/// Redaction event.
#[derive(Clone, Debug, Event)]
pub struct RedactionEvent {
/// Data specific to the event type.
pub content: RedactionEventContent,
/// The ID of the event that was redacted.
pub redacts: EventId,
/// The globally unique event identifier for the user who sent the event.
pub event_id: EventId,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// The ID of the room associated with this event.
pub room_id: RoomId,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// Redaction event without a `room_id`.
#[derive(Clone, Debug, Event)]
pub struct RedactionEventStub {
/// Data specific to the event type.
pub content: RedactionEventContent,
/// The ID of the event that was redacted.
pub redacts: EventId,
/// The globally unique event identifier for the user who sent the event.
pub event_id: EventId,
/// The fully-qualified ID of the user who sent this event.
pub sender: UserId,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: SystemTime,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: UnsignedData,
}
/// A redaction of an event.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.room.redaction")]
pub struct RedactionEventContent {
/// The reason for the redaction, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[cfg(test)]
mod tests {
use std::{
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use matches::assert_matches;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{RedactionEvent, RedactionEventContent};
use crate::{EventJson, UnsignedData};
#[test]
fn serialization() {
let event = RedactionEvent {
content: RedactionEventContent {
reason: Some("redacted because".into()),
},
redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(),
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
room_id: RoomId::try_from("!roomid:room.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
unsigned: UnsignedData::default(),
};
let json = json!({
"content": {
"reason": "redacted because"
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"redacts": "$h29iv0s8:example.com",
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"type": "m.room.redaction",
});
assert_eq!(to_json_value(&event).unwrap(), json);
}
#[test]
fn deserialization() {
let e_id = EventId::try_from("$h29iv0s8:example.com").unwrap();
let json = json!({
"content": {
"reason": "redacted because"
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"redacts": "$h29iv0s8:example.com",
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"type": "m.room.redaction",
});
assert_matches!(
from_json_value::<EventJson<RedactionEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
RedactionEvent {
content: RedactionEventContent {
reason: Some(reason),
},
sender,
event_id, origin_server_ts, redacts, room_id, unsigned,
} if reason == "redacted because" && redacts == e_id
&& event_id == e_id
&& sender == "@carl:example.com"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
&& unsigned.is_empty()
);
}
}

View File

@ -0,0 +1,73 @@
//! Types for the *m.room.server_acl* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// An event to indicate which servers are permitted to participate in the room.
pub type ServerAclEvent = StateEvent<ServerAclEventContent>;
/// The payload for `ServerAclEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.server_acl")]
pub struct ServerAclEventContent {
/// True to allow server names that are IP address literals. False to deny.
///
/// 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 = "ruma_serde::default_true",
skip_serializing_if = "ruma_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.
///
/// **This defaults to an empty list when not provided, effectively disallowing every
/// server.**
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allow: Vec<String>,
/// 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.
///
/// This defaults to an empty list when not provided.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deny: Vec<String>,
}
#[cfg(test)]
mod tests {
use serde_json::{from_value as from_json_value, json};
use super::ServerAclEventContent;
use crate::{EventJson, StateEvent};
#[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 =
from_json_value::<EventJson<StateEvent<ServerAclEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap();
assert_eq!(server_acl_event.content.allow_ip_literals, true);
assert!(server_acl_event.content.allow.is_empty());
assert!(server_acl_event.content.deny.is_empty());
}
}

View File

@ -0,0 +1,45 @@
//! Types for the *m.room.third_party_invite* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// 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.
pub type ThirdPartyInviteEvent = StateEvent<ThirdPartyInviteEventContent>;
/// The payload for `ThirdPartyInviteEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.third_party_invite")]
pub struct ThirdPartyInviteEventContent {
/// A user-readable string which represents the user who has been invited.
pub display_name: String,
/// A URL which can be fetched to validate whether the key has been revoked.
pub key_validity_url: String,
/// A Base64-encoded Ed25519 key with which the token must be signed.
pub public_key: String,
/// Keys with which the token may be signed.
#[serde(skip_serializing_if = "Option::is_none")]
pub public_keys: Option<Vec<PublicKey>>,
}
/// A public key for signing a third party invite token.
#[derive(Clone, Debug, Deserialize, Serialize)]
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<String>,
/// A Base64-encoded Ed25519 key with which the token must be signed.
pub public_key: String,
}

View File

@ -0,0 +1,22 @@
//! Types for the *m.room.tombstone* event.
use ruma_events_macros::StateEventContent;
use ruma_identifiers::RoomId;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// A state event signifying that a room has been upgraded to a different room version, and that
/// clients should go there.
pub type TombstoneEvent = StateEvent<TombstoneEventContent>;
/// The payload for `TombstoneEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.tombstone")]
pub struct TombstoneEventContent {
/// A server-defined message.
pub body: String,
/// The new room the client should be visiting.
pub replacement_room: RoomId,
}

View File

@ -0,0 +1,17 @@
//! Types for the *m.room.topic* event.
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
use crate::StateEvent;
/// A topic is a short message detailing what is currently being discussed in the room.
pub type TopicEvent = StateEvent<TopicEventContent>;
/// The payload for `TopicEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.room.topic")]
pub struct TopicEventContent {
/// The topic text.
pub topic: String,
}

View File

@ -0,0 +1,68 @@
//! Types for the *m.room_key* event.
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::RoomId;
use serde::{Deserialize, Serialize};
use super::Algorithm;
use crate::BasicEvent;
/// This event type is used to exchange keys for end-to-end encryption.
///
/// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event.
pub type RoomKeyEvent = BasicEvent<RoomKeyEventContent>;
/// The payload for `RoomKeyEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.room_key")]
pub struct RoomKeyEventContent {
/// The encryption algorithm the key in this event is to be used with.
///
/// Must be `m.megolm.v1.aes-sha2`.
pub algorithm: Algorithm,
/// The room where the key is used.
pub room_id: RoomId,
/// The ID of the session that the key is for.
pub session_id: String,
/// The key to be exchanged.
pub session_key: String,
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use ruma_identifiers::RoomId;
use serde_json::{json, to_value as to_json_value};
use super::RoomKeyEventContent;
use crate::{Algorithm, BasicEvent};
#[test]
fn serialization() {
let ev = BasicEvent {
content: RoomKeyEventContent {
algorithm: Algorithm::MegolmV1AesSha2,
room_id: RoomId::try_from("!testroomid:example.org").unwrap(),
session_id: "SessId".into(),
session_key: "SessKey".into(),
},
};
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",
},
})
);
}
}

View File

@ -0,0 +1,66 @@
//! Types for the *m.room_key_request* event.
use ruma_events_macros::BasicEventContent;
use ruma_identifiers::{DeviceId, RoomId};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use super::Algorithm;
use crate::BasicEvent;
/// This event type is used to request keys for end-to-end encryption.
///
/// It is sent as an unencrypted to-device event.
pub type RoomKeyRequestEvent = BasicEvent<RoomKeyRequestEventContent>;
/// The payload for `RoomKeyRequestEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.room_key_request")]
pub struct RoomKeyRequestEventContent {
/// Whether this is a new key request or a cancellation of a previous request.
pub action: Action,
/// Information about the requested key.
///
/// Required when action is `request`.
pub body: Option<RequestedKeyInfo>,
/// ID of the device requesting the key.
pub requesting_device_id: DeviceId,
/// 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: String,
}
/// A new key request or a cancellation of a previous request.
#[derive(Clone, Copy, Debug, PartialEq, Display, EnumString, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Action {
/// Request a key.
Request,
/// Cancel a request for a key.
#[serde(rename = "request_cancellation")]
#[strum(serialize = "request_cancellation")]
CancelRequest,
}
/// Information about a requested key.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RequestedKeyInfo {
/// The encryption algorithm the requested key in this event is to be used with.
pub algorithm: Algorithm,
/// The room where the key is used.
pub room_id: RoomId,
/// 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,
}

View File

@ -0,0 +1,25 @@
//! Types for the *m.sticker* event.
use ruma_events_macros::MessageEventContent;
use serde::{Deserialize, Serialize};
use crate::{room::ImageInfo, MessageEvent};
/// A sticker message.
pub type StickerEvent = MessageEvent<StickerEventContent>;
/// The payload for `StickerEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
#[ruma_event(type = "m.sticker")]
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. This must be a valid `mxc://` URI.
pub url: String,
}

310
ruma-events/src/stripped.rs Normal file
View File

@ -0,0 +1,310 @@
//! "Stripped-down" versions of the core state events.
//!
//! Each "stripped" event includes only the `content`, `type`, and `state_key` fields of its full
//! version. These stripped types are useful for APIs where the user is providing the content of a
//! state event to be created, when the other fields can be inferred from a larger context, or where
//! the other fields are otherwise inapplicable.
use ruma_identifiers::UserId;
use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize};
use serde_json::Value as JsonValue;
use crate::{
room::{
aliases::AliasesEventContent, avatar::AvatarEventContent,
canonical_alias::CanonicalAliasEventContent, create::CreateEventContent,
guest_access::GuestAccessEventContent, history_visibility::HistoryVisibilityEventContent,
join_rules::JoinRulesEventContent, member::MemberEventContent, name::NameEventContent,
power_levels::PowerLevelsEventContent, third_party_invite::ThirdPartyInviteEventContent,
topic::TopicEventContent,
},
util::get_field,
EventType, TryFromRaw,
};
/// A stripped-down version of a state event that is included along with some other events.
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum AnyStrippedStateEvent {
/// A stripped-down version of the *m.room.aliases* event.
RoomAliases(StrippedRoomAliases),
/// A stripped-down version of the *m.room.avatar* event.
RoomAvatar(StrippedRoomAvatar),
/// A stripped-down version of the *m.room.canonical_alias* event.
RoomCanonicalAlias(StrippedRoomCanonicalAlias),
/// A striped-down version of the *m.room.create* event.
RoomCreate(StrippedRoomCreate),
/// A stripped-down version of the *m.room.guest_access* event.
RoomGuestAccess(StrippedRoomGuestAccess),
/// A stripped-down version of the *m.room.history_visibility* event.
RoomHistoryVisibility(StrippedRoomHistoryVisibility),
/// A stripped-down version of the *m.room.join_rules* event.
RoomJoinRules(StrippedRoomJoinRules),
/// A stripped-down version of the *m.room.member* event.
RoomMember(StrippedRoomMember),
/// A stripped-down version of the *m.room.name* event.
RoomName(StrippedRoomName),
/// A stripped-down version of the *m.room.power_levels* event.
RoomPowerLevels(StrippedRoomPowerLevels),
/// A stripped-down version of the *m.room.third_party_invite* event.
RoomThirdPartyInvite(StrippedRoomThirdPartyInvite),
/// A stripped-down version of the *m.room.topic* event.
RoomTopic(StrippedRoomTopic),
}
/// A "stripped-down" version of a core state event.
#[derive(Clone, Debug, Serialize)]
pub struct StrippedStateEvent<C> {
/// Data specific to the event type.
pub content: C,
// FIXME(jplatte): It's unclear to me why this is stored
/// The type of the event.
#[serde(rename = "type")]
pub event_type: EventType,
/// A key that determines which piece of room state the event represents.
pub state_key: String,
/// The unique identifier for the user who sent this event.
pub sender: UserId,
}
/// A stripped-down version of the *m.room.aliases* event.
pub type StrippedRoomAliases = StrippedStateEvent<AliasesEventContent>;
/// A stripped-down version of the *m.room.avatar* event.
pub type StrippedRoomAvatar = StrippedStateEvent<AvatarEventContent>;
/// A stripped-down version of the *m.room.canonical_alias* event.
pub type StrippedRoomCanonicalAlias = StrippedStateEvent<CanonicalAliasEventContent>;
/// A stripped-down version of the *m.room.create* event.
pub type StrippedRoomCreate = StrippedStateEvent<CreateEventContent>;
/// A stripped-down version of the *m.room.guest_access* event.
pub type StrippedRoomGuestAccess = StrippedStateEvent<GuestAccessEventContent>;
/// A stripped-down version of the *m.room.history_visibility* event.
pub type StrippedRoomHistoryVisibility = StrippedStateEvent<HistoryVisibilityEventContent>;
/// A stripped-down version of the *m.room.join_rules* event.
pub type StrippedRoomJoinRules = StrippedStateEvent<JoinRulesEventContent>;
/// A stripped-down version of the *m.room.member* event.
pub type StrippedRoomMember = StrippedStateEvent<MemberEventContent>;
/// A stripped-down version of the *m.room.name* event.
pub type StrippedRoomName = StrippedStateEvent<NameEventContent>;
/// A stripped-down version of the *m.room.power_levels* event.
pub type StrippedRoomPowerLevels = StrippedStateEvent<PowerLevelsEventContent>;
/// A stripped-down version of the *m.room.third_party_invite* event.
pub type StrippedRoomThirdPartyInvite = StrippedStateEvent<ThirdPartyInviteEventContent>;
/// A stripped-down version of the *m.room.topic* event.
pub type StrippedRoomTopic = StrippedStateEvent<TopicEventContent>;
impl TryFromRaw for AnyStrippedStateEvent {
type Raw = raw::StrippedState;
type Err = String;
fn try_from_raw(raw: raw::StrippedState) -> Result<Self, Self::Err> {
use crate::util::try_convert_variant as conv;
use raw::StrippedState::*;
match raw {
RoomAliases(c) => conv(AnyStrippedStateEvent::RoomAliases, c),
RoomAvatar(c) => conv(AnyStrippedStateEvent::RoomAvatar, c),
RoomCanonicalAlias(c) => conv(AnyStrippedStateEvent::RoomCanonicalAlias, c),
RoomCreate(c) => conv(AnyStrippedStateEvent::RoomCreate, c),
RoomGuestAccess(c) => conv(AnyStrippedStateEvent::RoomGuestAccess, c),
RoomHistoryVisibility(c) => conv(AnyStrippedStateEvent::RoomHistoryVisibility, c),
RoomJoinRules(c) => conv(AnyStrippedStateEvent::RoomJoinRules, c),
RoomMember(c) => conv(AnyStrippedStateEvent::RoomMember, c),
RoomName(c) => conv(AnyStrippedStateEvent::RoomName, c),
RoomPowerLevels(c) => conv(AnyStrippedStateEvent::RoomPowerLevels, c),
RoomThirdPartyInvite(c) => conv(AnyStrippedStateEvent::RoomThirdPartyInvite, c),
RoomTopic(c) => conv(AnyStrippedStateEvent::RoomTopic, c),
}
}
}
impl<C> TryFromRaw for StrippedStateEvent<C>
where
C: TryFromRaw,
{
type Raw = StrippedStateEvent<C::Raw>;
type Err = C::Err;
fn try_from_raw(raw: StrippedStateEvent<C::Raw>) -> Result<Self, Self::Err> {
Ok(Self {
content: C::try_from_raw(raw.content)?,
event_type: raw.event_type,
state_key: raw.state_key,
sender: raw.sender,
})
}
}
impl<'de, C> Deserialize<'de> for StrippedStateEvent<C>
where
C: DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// TODO: Optimize
let value = JsonValue::deserialize(deserializer)?;
Ok(Self {
content: get_field(&value, "content")?,
event_type: get_field(&value, "type")?,
state_key: get_field(&value, "state_key")?,
sender: get_field(&value, "sender")?,
})
}
}
mod raw {
use serde::{Deserialize, Deserializer};
use serde_json::Value as JsonValue;
use super::StrippedStateEvent;
use crate::{
room::{
aliases::raw::AliasesEventContent, avatar::raw::AvatarEventContent,
canonical_alias::raw::CanonicalAliasEventContent, create::raw::CreateEventContent,
guest_access::raw::GuestAccessEventContent,
history_visibility::raw::HistoryVisibilityEventContent,
join_rules::raw::JoinRulesEventContent, member::raw::MemberEventContent,
name::raw::NameEventContent, power_levels::raw::PowerLevelsEventContent,
third_party_invite::raw::ThirdPartyInviteEventContent, topic::raw::TopicEventContent,
},
util::get_field,
};
/// A stripped-down version of the *m.room.aliases* event.
pub type StrippedRoomAliases = StrippedStateEvent<AliasesEventContent>;
/// A stripped-down version of the *m.room.avatar* event.
pub type StrippedRoomAvatar = StrippedStateEvent<AvatarEventContent>;
/// A stripped-down version of the *m.room.canonical_alias* event.
pub type StrippedRoomCanonicalAlias = StrippedStateEvent<CanonicalAliasEventContent>;
/// A stripped-down version of the *m.room.create* event.
pub type StrippedRoomCreate = StrippedStateEvent<CreateEventContent>;
/// A stripped-down version of the *m.room.guest_access* event.
pub type StrippedRoomGuestAccess = StrippedStateEvent<GuestAccessEventContent>;
/// A stripped-down version of the *m.room.history_visibility* event.
pub type StrippedRoomHistoryVisibility = StrippedStateEvent<HistoryVisibilityEventContent>;
/// A stripped-down version of the *m.room.join_rules* event.
pub type StrippedRoomJoinRules = StrippedStateEvent<JoinRulesEventContent>;
/// A stripped-down version of the *m.room.member* event.
pub type StrippedRoomMember = StrippedStateEvent<MemberEventContent>;
/// A stripped-down version of the *m.room.name* event.
pub type StrippedRoomName = StrippedStateEvent<NameEventContent>;
/// A stripped-down version of the *m.room.power_levels* event.
pub type StrippedRoomPowerLevels = StrippedStateEvent<PowerLevelsEventContent>;
/// A stripped-down version of the *m.room.third_party_invite* event.
pub type StrippedRoomThirdPartyInvite = StrippedStateEvent<ThirdPartyInviteEventContent>;
/// A stripped-down version of the *m.room.topic* event.
pub type StrippedRoomTopic = StrippedStateEvent<TopicEventContent>;
/// A stripped-down version of a state event that is included along with some other events.
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum StrippedState {
/// A stripped-down version of the *m.room.aliases* event.
RoomAliases(StrippedRoomAliases),
/// A stripped-down version of the *m.room.avatar* event.
RoomAvatar(StrippedRoomAvatar),
/// A stripped-down version of the *m.room.canonical_alias* event.
RoomCanonicalAlias(StrippedRoomCanonicalAlias),
/// A striped-down version of the *m.room.create* event.
RoomCreate(StrippedRoomCreate),
/// A stripped-down version of the *m.room.guest_access* event.
RoomGuestAccess(StrippedRoomGuestAccess),
/// A stripped-down version of the *m.room.history_visibility* event.
RoomHistoryVisibility(StrippedRoomHistoryVisibility),
/// A stripped-down version of the *m.room.join_rules* event.
RoomJoinRules(StrippedRoomJoinRules),
/// A stripped-down version of the *m.room.member* event.
RoomMember(StrippedRoomMember),
/// A stripped-down version of the *m.room.name* event.
RoomName(StrippedRoomName),
/// A stripped-down version of the *m.room.power_levels* event.
RoomPowerLevels(StrippedRoomPowerLevels),
/// A stripped-down version of the *m.room.third_party_invite* event.
RoomThirdPartyInvite(StrippedRoomThirdPartyInvite),
/// A stripped-down version of the *m.room.topic* event.
RoomTopic(StrippedRoomTopic),
}
impl<'de> Deserialize<'de> for StrippedState {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use crate::{util::try_variant_from_value as from_value, EventType::*};
use serde::de::Error as _;
// TODO: Optimize
let value = JsonValue::deserialize(deserializer)?;
let event_type = get_field(&value, "type")?;
match event_type {
RoomAliases => from_value(value, StrippedState::RoomAliases),
RoomAvatar => from_value(value, StrippedState::RoomAvatar),
RoomCanonicalAlias => from_value(value, StrippedState::RoomCanonicalAlias),
RoomCreate => from_value(value, StrippedState::RoomCreate),
RoomGuestAccess => from_value(value, StrippedState::RoomGuestAccess),
RoomHistoryVisibility => from_value(value, StrippedState::RoomHistoryVisibility),
RoomJoinRules => from_value(value, StrippedState::RoomJoinRules),
RoomMember => from_value(value, StrippedState::RoomMember),
RoomName => from_value(value, StrippedState::RoomName),
RoomPowerLevels => from_value(value, StrippedState::RoomPowerLevels),
RoomThirdPartyInvite => from_value(value, StrippedState::RoomThirdPartyInvite),
RoomTopic => from_value(value, StrippedState::RoomTopic),
_ => Err(D::Error::custom("not a state event")),
}
}
}
}
#[cfg(test)]
mod tests {}

27
ruma-events/src/tag.rs Normal file
View File

@ -0,0 +1,27 @@
//! Types for the *m.tag* event.
use std::collections::BTreeMap;
use ruma_events_macros::BasicEventContent;
use serde::{Deserialize, Serialize};
use crate::BasicEvent;
/// Informs the client of tags on a room.
pub type TagEvent = BasicEvent<TagEventContent>;
/// The payload for `TagEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)]
#[ruma_event(type = "m.tag")]
pub struct TagEventContent {
/// A map of tag names to tag info.
pub tags: BTreeMap<String, TagInfo>,
}
/// Information about a tag.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TagInfo {
/// Value to use for lexicographically ordering rooms with this tag.
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<f64>,
}

18
ruma-events/src/typing.rs Normal file
View File

@ -0,0 +1,18 @@
//! Types for the *m.typing* event.
use ruma_events_macros::EphemeralRoomEventContent;
use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
use crate::EphemeralRoomEvent;
/// Informs the client who is currently typing in a given room.
pub type TypingEvent = EphemeralRoomEvent<TypingEventContent>;
/// The payload for `TypingEvent`.
#[derive(Clone, Debug, Deserialize, Serialize, EphemeralRoomEventContent)]
#[ruma_event(type = "m.typing")]
pub struct TypingEventContent {
/// The list of user IDs typing in this room, if any.
pub user_ids: Vec<UserId>,
}

30
ruma-events/src/util.rs Normal file
View File

@ -0,0 +1,30 @@
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
pub fn try_variant_from_value<T, U, E>(value: JsonValue, variant: fn(T) -> U) -> Result<U, E>
where
T: DeserializeOwned,
E: serde::de::Error,
{
serde_json::from_value(value)
.map(variant)
.map_err(serde_json_error_to_generic_de_error)
}
pub fn serde_json_error_to_generic_de_error<E: serde::de::Error>(error: serde_json::Error) -> E {
E::custom(error.to_string())
}
pub fn get_field<T, E>(value: &JsonValue, field: &'static str) -> Result<T, E>
where
T: DeserializeOwned,
E: serde::de::Error,
{
serde_json::from_value(
value
.get(field)
.cloned()
.ok_or_else(|| E::missing_field(field))?,
)
.map_err(serde_json_error_to_generic_de_error)
}

View File

@ -0,0 +1,129 @@
use std::{
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use maplit::btreemap;
use matches::assert_matches;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use ruma_events::{
receipt::{Receipt, ReceiptEventContent, Receipts},
typing::TypingEventContent,
AnyEphemeralRoomEventContent, EphemeralRoomEvent, EventJson,
};
#[test]
fn ephemeral_serialize_typing() {
let aliases_event = EphemeralRoomEvent {
content: AnyEphemeralRoomEventContent::Typing(TypingEventContent {
user_ids: vec![UserId::try_from("@carl:example.com").unwrap()],
}),
room_id: RoomId::try_from("!roomid:room.com").unwrap(),
};
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"
});
assert_matches!(
from_json_value::<EventJson<EphemeralRoomEvent<AnyEphemeralRoomEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
EphemeralRoomEvent {
content: AnyEphemeralRoomEventContent::Typing(TypingEventContent {
user_ids,
}),
room_id,
} if user_ids[0] == UserId::try_from("@carl:example.com").unwrap()
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
);
}
#[test]
fn ephemeral_serialize_receipt() {
let event_id = EventId::try_from("$h29iv0s8:example.com").unwrap();
let user_id = UserId::try_from("@carl:example.com").unwrap();
let aliases_event = EphemeralRoomEvent {
content: AnyEphemeralRoomEventContent::Receipt(ReceiptEventContent(btreemap! {
event_id => Receipts {
read: Some(btreemap! {
user_id => Receipt { ts: Some(UNIX_EPOCH + Duration::from_millis(1)) },
}),
},
})),
room_id: RoomId::try_from("!roomid:room.com").unwrap(),
};
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 = EventId::try_from("$h29iv0s8:example.com").unwrap();
let user_id = UserId::try_from("@carl:example.com").unwrap();
let json_data = json!({
"content": {
"$h29iv0s8:example.com": {
"m.read": {
"@carl:example.com": { "ts": 1 }
}
}
},
"room_id": "!roomid:room.com",
"type": "m.receipt"
});
assert_matches!(
from_json_value::<EventJson<EphemeralRoomEvent<AnyEphemeralRoomEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
EphemeralRoomEvent {
content: AnyEphemeralRoomEventContent::Receipt(ReceiptEventContent(receipts)),
room_id,
} if !receipts.is_empty() && receipts.contains_key(&event_id)
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
&& receipts
.get(&event_id)
.map(|r| r.read.as_ref().unwrap().get(&user_id).unwrap().clone())
.map(|r| r.ts)
.unwrap()
== Some(UNIX_EPOCH + Duration::from_millis(1))
);
}

View File

@ -0,0 +1,10 @@
#[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/ui/04-event-sanity-check.rs");
t.compile_fail("tests/ui/05-named-fields.rs");
t.compile_fail("tests/ui/06-no-content-field.rs");
}

View File

@ -0,0 +1,7 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/01-content-sanity-check.rs");
t.compile_fail("tests/ui/02-no-event-type.rs");
t.compile_fail("tests/ui/03-invalid-event-type.rs");
}

View File

@ -0,0 +1,6 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/07-enum-sanity-check.rs");
t.compile_fail("tests/ui/08-enum-invalid-path.rs");
}

View File

@ -0,0 +1,223 @@
use std::{
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use js_int::UInt;
use matches::assert_matches;
use ruma_events::{
call::{answer::AnswerEventContent, SessionDescription, SessionDescriptionType},
room::{ImageInfo, ThumbnailInfo},
sticker::StickerEventContent,
AnyMessageEventContent, EventJson, MessageEvent, UnsignedData,
};
use ruma_identifiers::{EventId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test]
fn message_serialize_sticker() {
let aliases_event = MessageEvent {
content: AnyMessageEventContent::Sticker(StickerEventContent {
body: "Hello".into(),
info: ImageInfo {
height: UInt::new(423),
width: UInt::new(1011),
mimetype: Some("image/png".into()),
size: UInt::new(84242),
thumbnail_info: Some(Box::new(ThumbnailInfo {
width: UInt::new(800),
height: UInt::new(334),
mimetype: Some("image/png".into()),
size: UInt::new(82595),
})),
thumbnail_url: Some("mxc://matrix.org".into()),
thumbnail_file: None,
},
url: "http://www.matrix.org".into(),
}),
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
room_id: RoomId::try_from("!roomid:room.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
unsigned: UnsignedData::default(),
};
let actual = to_json_value(&aliases_event).unwrap();
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",
"w": 1011
},
"url": "http://www.matrix.org"
},
"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": 1
});
assert_matches!(
from_json_value::<EventJson<AnyMessageEventContent>>(json_data)
.unwrap()
.deserialize_content("m.call.answer")
.unwrap(),
AnyMessageEventContent::CallAnswer(AnswerEventContent {
answer: SessionDescription {
session_type: SessionDescriptionType::Answer,
sdp,
},
call_id,
version,
}) if sdp == "Hello" && call_id == "foofoo" && version == UInt::new(1).unwrap()
);
}
#[test]
fn deserialize_message_call_answer() {
let json_data = json!({
"content": {
"answer": {
"type": "answer",
"sdp": "Hello"
},
"call_id": "foofoo",
"version": 1
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"type": "m.call.answer"
});
assert_matches!(
from_json_value::<EventJson<MessageEvent<AnyMessageEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
MessageEvent {
content: AnyMessageEventContent::CallAnswer(AnswerEventContent {
answer: SessionDescription {
session_type: SessionDescriptionType::Answer,
sdp,
},
call_id,
version,
}),
event_id,
origin_server_ts,
room_id,
sender,
unsigned,
} if sdp == "Hello" && call_id == "foofoo" && version == UInt::new(1).unwrap()
&& event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
&& sender == UserId::try_from("@carl:example.com").unwrap()
&& unsigned.is_empty()
);
}
#[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",
"w": 1011
},
"url": "http://www.matrix.org"
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"type": "m.sticker"
});
assert_matches!(
from_json_value::<EventJson<MessageEvent<AnyMessageEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
MessageEvent {
content: AnyMessageEventContent::Sticker(StickerEventContent {
body,
info: ImageInfo {
height,
width,
mimetype: Some(mimetype),
size,
thumbnail_info: Some(thumbnail_info),
thumbnail_url: Some(thumbnail_url),
thumbnail_file: None,
},
url,
}),
event_id,
origin_server_ts,
room_id,
sender,
unsigned
} if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
&& body == "Hello"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
&& sender == UserId::try_from("@carl:example.com").unwrap()
&& height == UInt::new(423)
&& width == UInt::new(1011)
&& mimetype == "image/png"
&& size == UInt::new(84242)
&& thumbnail_url == "mxc://matrix.org"
&& matches!(
thumbnail_info.as_ref(),
ThumbnailInfo {
width: thumb_width,
height: thumb_height,
mimetype: thumb_mimetype,
size: thumb_size,
} if *thumb_width == UInt::new(800)
&& *thumb_height == UInt::new(334)
&& *thumb_mimetype == Some("image/png".to_string())
&& *thumb_size == UInt::new(82595)
)
&& url == "http://www.matrix.org"
&& unsigned.is_empty()
);
}

View File

@ -0,0 +1,220 @@
use std::{
convert::TryFrom,
time::{Duration, UNIX_EPOCH},
};
use js_int::UInt;
use matches::assert_matches;
use ruma_events::{
room::{aliases::AliasesEventContent, avatar::AvatarEventContent, ImageInfo, ThumbnailInfo},
AnyStateEventContent, EventJson, StateEvent, UnsignedData,
};
use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test]
fn serialize_aliases_with_prev_content() {
let aliases_event = StateEvent {
content: AnyStateEventContent::RoomAliases(AliasesEventContent {
aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()],
}),
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: Some(AnyStateEventContent::RoomAliases(AliasesEventContent {
aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()],
})),
room_id: RoomId::try_from("!roomid:room.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "".to_string(),
unsigned: UnsignedData::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,
"prev_content": {
"aliases": [ "#somewhere:localhost" ]
},
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"state_key": "",
"type": "m.room.aliases",
});
assert_eq!(actual, expected);
}
#[test]
fn serialize_aliases_without_prev_content() {
let aliases_event = StateEvent {
content: AnyStateEventContent::RoomAliases(AliasesEventContent {
aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()],
}),
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
prev_content: None,
room_id: RoomId::try_from("!roomid:room.com").unwrap(),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "".to_string(),
unsigned: UnsignedData::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": "",
"type": "m.room.aliases",
});
assert_eq!(actual, expected);
}
#[test]
fn deserialize_aliases_content() {
let json_data = json!({
"aliases": [ "#somewhere:localhost" ]
});
assert_matches!(
from_json_value::<EventJson<AnyStateEventContent>>(json_data)
.unwrap()
.deserialize_content("m.room.aliases")
.unwrap(),
AnyStateEventContent::RoomAliases(content)
if content.aliases == vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()]
);
}
#[test]
fn deserialize_aliases_with_prev_content() {
let json_data = json!({
"content": {
"aliases": [ "#somewhere:localhost" ]
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"prev_content": {
"aliases": [ "#inner:localhost" ]
},
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"state_key": "",
"type": "m.room.aliases"
});
assert_matches!(
from_json_value::<EventJson<StateEvent<AnyStateEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
StateEvent {
content: AnyStateEventContent::RoomAliases(content),
event_id,
origin_server_ts,
prev_content: Some(AnyStateEventContent::RoomAliases(prev_content)),
room_id,
sender,
state_key,
unsigned,
} if content.aliases == vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()]
&& event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& prev_content.aliases == vec![RoomAliasId::try_from("#inner:localhost").unwrap()]
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
&& sender == UserId::try_from("@carl:example.com").unwrap()
&& state_key == ""
&& unsigned.is_empty()
);
}
#[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",
"w": 1011
},
"url": "http://www.matrix.org"
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"room_id": "!roomid:room.com",
"sender": "@carl:example.com",
"state_key": "",
"type": "m.room.avatar"
});
assert_matches!(
from_json_value::<EventJson<StateEvent<AnyStateEventContent>>>(json_data)
.unwrap()
.deserialize()
.unwrap(),
StateEvent {
content: AnyStateEventContent::RoomAvatar(AvatarEventContent {
info: Some(info),
url,
}),
event_id,
origin_server_ts,
prev_content: None,
room_id,
sender,
state_key,
unsigned
} if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == RoomId::try_from("!roomid:room.com").unwrap()
&& sender == UserId::try_from("@carl:example.com").unwrap()
&& state_key == ""
&& matches!(
info.as_ref(),
ImageInfo {
height,
width,
mimetype: Some(mimetype),
size,
thumbnail_info: Some(thumbnail_info),
thumbnail_url: Some(thumbnail_url),
thumbnail_file: None,
} if *height == UInt::new(423)
&& *width == UInt::new(1011)
&& *mimetype == "image/png"
&& *size == UInt::new(84242)
&& matches!(
thumbnail_info.as_ref(),
ThumbnailInfo {
width: thumb_width,
height: thumb_height,
mimetype: thumb_mimetype,
size: thumb_size,
} if *thumb_width == UInt::new(800)
&& *thumb_height == UInt::new(334)
&& *thumb_mimetype == Some("image/png".to_string())
&& *thumb_size == UInt::new(82595)
&& *thumbnail_url == "mxc://matrix.org"
)
)
&& url == "http://www.matrix.org"
&& unsigned.is_empty()
);
}

View File

@ -0,0 +1,138 @@
use std::convert::TryFrom;
use js_int::UInt;
use ruma_events::{
room::{join_rules::JoinRule, topic::TopicEventContent},
AnyStrippedStateEvent, EventJson, EventType,
};
use ruma_identifiers::UserId;
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test]
fn serialize_stripped_state_event() {
let content = StrippedRoomTopic {
content: TopicEventContent {
topic: "Testing room".to_string(),
},
state_key: "".to_string(),
event_type: EventType::RoomTopic,
sender: UserId::try_from("@example:localhost").unwrap(),
};
let event = AnyStrippedStateEvent::RoomTopic(content);
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": "https://example.com/image-thumbnail.jpg"
},
"thumbnail_info": {
"h": 16,
"w": 16,
"mimetype": "image/jpeg",
"size": 32
},
"thumbnail_url": "https://example.com/image-thumbnail.jpg",
"url": "https://example.com/image.jpg"
}
});
match from_json_value::<EventJson<_>>(name_event.clone())
.unwrap()
.deserialize()
.unwrap()
{
AnyStrippedStateEvent::RoomName(event) => {
assert_eq!(event.content.name, Some("Ruma".to_string()));
assert_eq!(event.event_type, EventType::RoomName);
assert_eq!(event.state_key, "");
assert_eq!(event.sender.to_string(), "@example:localhost");
}
_ => unreachable!(),
};
// Ensure `StrippedStateContent` can be parsed, not just `StrippedState`.
assert!(from_json_value::<EventJson<StrippedRoomName>>(name_event)
.unwrap()
.deserialize()
.is_ok());
match from_json_value::<EventJson<_>>(join_rules_event)
.unwrap()
.deserialize()
.unwrap()
{
AnyStrippedStateEvent::RoomJoinRules(event) => {
assert_eq!(event.content.join_rule, JoinRule::Public);
assert_eq!(event.event_type, EventType::RoomJoinRules);
assert_eq!(event.state_key, "");
assert_eq!(event.sender.to_string(), "@example:localhost");
}
_ => unreachable!(),
};
match from_json_value::<EventJson<_>>(avatar_event)
.unwrap()
.deserialize()
.unwrap()
{
AnyStrippedStateEvent::RoomAvatar(event) => {
let image_info = event.content.info.unwrap();
assert_eq!(image_info.height.unwrap(), UInt::try_from(128).unwrap());
assert_eq!(image_info.width.unwrap(), UInt::try_from(128).unwrap());
assert_eq!(image_info.mimetype.unwrap(), "image/jpeg");
assert_eq!(image_info.size.unwrap(), UInt::try_from(1024).unwrap());
assert_eq!(
image_info.thumbnail_info.unwrap().size.unwrap(),
UInt::try_from(32).unwrap()
);
assert_eq!(event.content.url, "https://example.com/image.jpg");
assert_eq!(event.event_type, EventType::RoomAvatar);
assert_eq!(event.state_key, "");
assert_eq!(event.sender.to_string(), "@example:localhost");
}
_ => unreachable!(),
};
}

View File

@ -0,0 +1,34 @@
use std::convert::TryFrom;
use ruma_events::{
room_key::RoomKeyEventContent, Algorithm, AnyToDeviceEventContent, ToDeviceEvent,
};
use ruma_identifiers::{RoomId, UserId};
use serde_json::{json, to_value as to_json_value};
#[test]
fn serialization() {
let ev = ToDeviceEvent {
sender: UserId::try_from("@example:example.org").unwrap(),
content: AnyToDeviceEventContent::RoomKey(RoomKeyEventContent {
algorithm: Algorithm::MegolmV1AesSha2,
room_id: RoomId::try_from("!testroomid:example.org").unwrap(),
session_id: "SessId".into(),
session_key: "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",
},
})
);
}

View File

@ -0,0 +1,10 @@
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(type = "m.macro.test")]
pub struct MacroTest {
pub url: String,
}
fn main() {}

View File

@ -0,0 +1,9 @@
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
pub struct MacroTest {
pub url: String,
}
fn main() {}

View File

@ -0,0 +1,7 @@
error: no event type attribute found, add `#[ruma_event(type = "any.room.event")]` below the event content derive
--> $DIR/02-no-event-type.rs:4:48
|
4 | #[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
| ^^^^^^^^^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,16 @@
use ruma_events_macros::StateEventContent;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[not_ruma_event(type = "m.macro.test")]
pub struct MacroTest {
pub test: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
#[ruma_event(event = "m.macro.test")]
pub struct MoreMacroTest {
pub test: String,
}
fn main() {}

View File

@ -0,0 +1,19 @@
error: expected `type`
--> $DIR/03-invalid-event-type.rs:11:14
|
11 | #[ruma_event(event = "m.macro.test")]
| ^^^^^
error: no event type attribute found, add `#[ruma_event(type = "any.room.event")]` below the event content derive
--> $DIR/03-invalid-event-type.rs:4:48
|
4 | #[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)]
| ^^^^^^^^^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: cannot find attribute `not_ruma_event` in this scope
--> $DIR/03-invalid-event-type.rs:5:3
|
5 | #[not_ruma_event(type = "m.macro.test")]
| ^^^^^^^^^^^^^^ help: a derive helper attribute with a similar name exists: `ruma_event`

View File

@ -0,0 +1,16 @@
// rustc overflows when compiling this see:
// https://github.com/rust-lang/rust/issues/55779
extern crate serde;
use ruma_events::StateEventContent;
use ruma_events_macros::Event;
/// State event.
#[derive(Clone, Debug, Event)]
pub struct StateEvent<C: StateEventContent> {
pub content: C,
pub state_key: String,
pub prev_content: Option<C>,
}
fn main() {}

View File

@ -0,0 +1,8 @@
use ruma_events::StateEventContent;
use ruma_events_macros::Event;
/// State event.
#[derive(Clone, Debug, Event)]
pub struct StateEvent<C: StateEventContent>(C);
fn main() {}

View File

@ -0,0 +1,5 @@
error: the `Event` derive only supports named fields
--> $DIR/05-named-fields.rs:6:44
|
6 | pub struct StateEvent<C: StateEventContent>(C);
| ^^^

View File

@ -0,0 +1,10 @@
use ruma_events::StateEventContent;
use ruma_events_macros::Event;
/// State event.
#[derive(Clone, Debug, Event)]
pub struct StateEvent<C: StateEventContent> {
pub not_content: C,
}
fn main() {}

View File

@ -0,0 +1,7 @@
error: struct must contain a `content` field
--> $DIR/06-no-content-field.rs:5:24
|
5 | #[derive(Clone, Debug, Event)]
| ^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,15 @@
use ruma_events_macros::event_content_enum;
event_content_enum! {
/// Any basic event.
name: AnyBasicEventContent,
events: [
"m.direct",
"m.dummy",
"m.ignored_user_list",
"m.push_rules",
"m.room_key",
]
}
fn main() {}

View File

@ -0,0 +1,17 @@
use ruma_events_macros::event_content_enum;
event_content_enum! {
name: InvalidEvent,
events: [
"m.not.a.path",
]
}
event_content_enum! {
name: InvalidEvent,
events: [
"not.a.path",
]
}
fn main() {}

View File

@ -0,0 +1,18 @@
error: proc macro panicked
--> $DIR/08-enum-invalid-path.rs:10:1
|
10 | / event_content_enum! {
11 | | name: InvalidEvent,
12 | | events: [
13 | | "not.a.path",
14 | | ]
15 | | }
| |_^
|
= help: message: well-known matrix events have to start with `m.` found `not.a.path`
error[E0433]: failed to resolve: could not find `not` in `ruma_events`
--> $DIR/08-enum-invalid-path.rs:6:9
|
6 | "m.not.a.path",
| ^^^^^^^^^^^^^^ could not find `not` in `ruma_events`