Add 'ruma-events/' from commit '00692d532e26f58d48ead9589dc823403c6e59a5'
git-subtree-dir: ruma-events git-subtree-mainline: d59a616e2c363507a89c92f34aa67e86ee2cfb49 git-subtree-split: 00692d532e26f58d48ead9589dc823403c6e59a5
This commit is contained in:
commit
f304c04d1d
27
ruma-events/.builds/beta.yml
Normal file
27
ruma-events/.builds/beta.yml
Normal 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 ))
|
16
ruma-events/.builds/msrv.yml
Normal file
16
ruma-events/.builds/msrv.yml
Normal 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
|
32
ruma-events/.builds/nightly.yml
Normal file
32
ruma-events/.builds/nightly.yml
Normal 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 ))
|
29
ruma-events/.builds/stable.yml
Normal file
29
ruma-events/.builds/stable.yml
Normal 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
5
ruma-events/.gitignore
vendored
Normal 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
312
ruma-events/CHANGELOG.md
Normal 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
34
ruma-events/Cargo.toml
Normal 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
20
ruma-events/LICENSE
Normal 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
15
ruma-events/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# ruma-events
|
||||||
|
|
||||||
|
[](https://crates.io/crates/ruma-events)
|
||||||
|
[](https://docs.rs/ruma-events/)
|
||||||
|

|
||||||
|
|
||||||
|
**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.
|
20
ruma-events/ruma-events-macros/CHANGELOG.md
Normal file
20
ruma-events/ruma-events-macros/CHANGELOG.md
Normal 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)
|
27
ruma-events/ruma-events-macros/Cargo.toml
Normal file
27
ruma-events/ruma-events-macros/Cargo.toml
Normal 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"] }
|
13
ruma-events/ruma-events-macros/README.md
Normal file
13
ruma-events/ruma-events-macros/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ruma-events-macros
|
||||||
|
|
||||||
|
[](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)
|
192
ruma-events/ruma-events-macros/src/content_enum.rs
Normal file
192
ruma-events/ruma-events-macros/src/content_enum.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
287
ruma-events/ruma-events-macros/src/event.rs
Normal file
287
ruma-events/ruma-events-macros/src/event.rs
Normal 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", ×tamp)?;
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
126
ruma-events/ruma-events-macros/src/event_content.rs
Normal file
126
ruma-events/ruma-events-macros/src/event_content.rs
Normal 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 { }
|
||||||
|
})
|
||||||
|
}
|
102
ruma-events/ruma-events-macros/src/lib.rs
Normal file
102
ruma-events/ruma-events-macros/src/lib.rs
Normal 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()
|
||||||
|
}
|
70
ruma-events/src/algorithm.rs
Normal file
70
ruma-events/src/algorithm.rs
Normal 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
35
ruma-events/src/call.rs
Normal 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,
|
||||||
|
}
|
25
ruma-events/src/call/answer.rs
Normal file
25
ruma-events/src/call/answer.rs
Normal 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,
|
||||||
|
}
|
39
ruma-events/src/call/candidates.rs
Normal file
39
ruma-events/src/call/candidates.rs
Normal 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,
|
||||||
|
}
|
43
ruma-events/src/call/hangup.rs
Normal file
43
ruma-events/src/call/hangup.rs
Normal 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,
|
||||||
|
}
|
30
ruma-events/src/call/invite.rs
Normal file
30
ruma-events/src/call/invite.rs
Normal 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
76
ruma-events/src/custom.rs
Normal 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
90
ruma-events/src/direct.rs
Normal 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
75
ruma-events/src/dummy.rs
Normal 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
157
ruma-events/src/enums.rs
Normal 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
89
ruma-events/src/error.rs
Normal 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 {}
|
160
ruma-events/src/event_kinds.rs
Normal file
160
ruma-events/src/event_kinds.rs
Normal 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,
|
||||||
|
}
|
344
ruma-events/src/event_type.rs
Normal file
344
ruma-events/src/event_type.rs
Normal 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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
ruma-events/src/forwarded_room_key.rs
Normal file
48
ruma-events/src/forwarded_room_key.rs
Normal 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>,
|
||||||
|
}
|
21
ruma-events/src/fully_read.rs
Normal file
21
ruma-events/src/fully_read.rs
Normal 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,
|
||||||
|
}
|
73
ruma-events/src/ignored_user_list.rs
Normal file
73
ruma-events/src/ignored_user_list.rs
Normal 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
124
ruma-events/src/json.rs
Normal 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
3
ruma-events/src/key.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
//! Modules for events in the *m.key* namespace.
|
||||||
|
|
||||||
|
pub mod verification;
|
61
ruma-events/src/key/verification.rs
Normal file
61
ruma-events/src/key/verification.rs
Normal 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,
|
||||||
|
}
|
52
ruma-events/src/key/verification/accept.rs
Normal file
52
ruma-events/src/key/verification/accept.rs
Normal 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,
|
||||||
|
}
|
155
ruma-events/src/key/verification/cancel.rs
Normal file
155
ruma-events/src/key/verification/cancel.rs
Normal 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())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
24
ruma-events/src/key/verification/key.rs
Normal file
24
ruma-events/src/key/verification/key.rs
Normal 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,
|
||||||
|
}
|
32
ruma-events/src/key/verification/mac.rs
Normal file
32
ruma-events/src/key/verification/mac.rs
Normal 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,
|
||||||
|
}
|
38
ruma-events/src/key/verification/request.rs
Normal file
38
ruma-events/src/key/verification/request.rs
Normal 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,
|
||||||
|
}
|
464
ruma-events/src/key/verification/start.rs
Normal file
464
ruma-events/src/key/verification/start.rs
Normal 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
234
ruma-events/src/lib.rs
Normal 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
128
ruma-events/src/presence.rs
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
464
ruma-events/src/push_rules.rs
Normal file
464
ruma-events/src/push_rules.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
63
ruma-events/src/receipt.rs
Normal file
63
ruma-events/src/receipt.rs
Normal 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
120
ruma-events/src/room.rs
Normal 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,
|
||||||
|
}
|
18
ruma-events/src/room/aliases.rs
Normal file
18
ruma-events/src/room/aliases.rs
Normal 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>,
|
||||||
|
}
|
25
ruma-events/src/room/avatar.rs
Normal file
25
ruma-events/src/room/avatar.rs
Normal 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,
|
||||||
|
}
|
172
ruma-events/src/room/canonical_alias.rs
Normal file
172
ruma-events/src/room/canonical_alias.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
105
ruma-events/src/room/create.rs
Normal file
105
ruma-events/src/room/create.rs
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
165
ruma-events/src/room/encrypted.rs
Normal file
165
ruma-events/src/room/encrypted.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
30
ruma-events/src/room/encryption.rs
Normal file
30
ruma-events/src/room/encryption.rs
Normal 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>,
|
||||||
|
}
|
34
ruma-events/src/room/guest_access.rs
Normal file
34
ruma-events/src/room/guest_access.rs
Normal 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,
|
||||||
|
}
|
43
ruma-events/src/room/history_visibility.rs
Normal file
43
ruma-events/src/room/history_visibility.rs
Normal 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,
|
||||||
|
}
|
37
ruma-events/src/room/join_rules.rs
Normal file
37
ruma-events/src/room/join_rules.rs
Normal 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,
|
||||||
|
}
|
468
ruma-events/src/room/member.rs
Normal file
468
ruma-events/src/room/member.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
559
ruma-events/src/room/message.rs
Normal file
559
ruma-events/src/room/message.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
38
ruma-events/src/room/message/feedback.rs
Normal file
38
ruma-events/src/room/message/feedback.rs
Normal 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,
|
||||||
|
}
|
275
ruma-events/src/room/name.rs
Normal file
275
ruma-events/src/room/name.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
ruma-events/src/room/pinned_events.rs
Normal file
66
ruma-events/src/room/pinned_events.rs
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
286
ruma-events/src/room/power_levels.rs
Normal file
286
ruma-events/src/room/power_levels.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
145
ruma-events/src/room/redaction.rs
Normal file
145
ruma-events/src/room/redaction.rs
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
73
ruma-events/src/room/server_acl.rs
Normal file
73
ruma-events/src/room/server_acl.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
45
ruma-events/src/room/third_party_invite.rs
Normal file
45
ruma-events/src/room/third_party_invite.rs
Normal 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,
|
||||||
|
}
|
22
ruma-events/src/room/tombstone.rs
Normal file
22
ruma-events/src/room/tombstone.rs
Normal 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,
|
||||||
|
}
|
17
ruma-events/src/room/topic.rs
Normal file
17
ruma-events/src/room/topic.rs
Normal 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,
|
||||||
|
}
|
68
ruma-events/src/room_key.rs
Normal file
68
ruma-events/src/room_key.rs
Normal 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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
ruma-events/src/room_key_request.rs
Normal file
66
ruma-events/src/room_key_request.rs
Normal 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,
|
||||||
|
}
|
25
ruma-events/src/sticker.rs
Normal file
25
ruma-events/src/sticker.rs
Normal 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
310
ruma-events/src/stripped.rs
Normal 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
27
ruma-events/src/tag.rs
Normal 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
18
ruma-events/src/typing.rs
Normal 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
30
ruma-events/src/util.rs
Normal 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)
|
||||||
|
}
|
129
ruma-events/tests/ephemeral_event.rs
Normal file
129
ruma-events/tests/ephemeral_event.rs
Normal 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))
|
||||||
|
);
|
||||||
|
}
|
10
ruma-events/tests/event.rs
Normal file
10
ruma-events/tests/event.rs
Normal 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");
|
||||||
|
}
|
7
ruma-events/tests/event_content.rs
Normal file
7
ruma-events/tests/event_content.rs
Normal 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");
|
||||||
|
}
|
6
ruma-events/tests/event_content_enum.rs
Normal file
6
ruma-events/tests/event_content_enum.rs
Normal 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");
|
||||||
|
}
|
223
ruma-events/tests/message_event.rs
Normal file
223
ruma-events/tests/message_event.rs
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
220
ruma-events/tests/state_event.rs
Normal file
220
ruma-events/tests/state_event.rs
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
138
ruma-events/tests/stripped.rs.bk
Normal file
138
ruma-events/tests/stripped.rs.bk
Normal 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!(),
|
||||||
|
};
|
||||||
|
}
|
34
ruma-events/tests/to_device.rs
Normal file
34
ruma-events/tests/to_device.rs
Normal 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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
10
ruma-events/tests/ui/01-content-sanity-check.rs
Normal file
10
ruma-events/tests/ui/01-content-sanity-check.rs
Normal 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() {}
|
9
ruma-events/tests/ui/02-no-event-type.rs
Normal file
9
ruma-events/tests/ui/02-no-event-type.rs
Normal 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() {}
|
7
ruma-events/tests/ui/02-no-event-type.stderr
Normal file
7
ruma-events/tests/ui/02-no-event-type.stderr
Normal 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)
|
16
ruma-events/tests/ui/03-invalid-event-type.rs
Normal file
16
ruma-events/tests/ui/03-invalid-event-type.rs
Normal 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() {}
|
19
ruma-events/tests/ui/03-invalid-event-type.stderr
Normal file
19
ruma-events/tests/ui/03-invalid-event-type.stderr
Normal 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`
|
16
ruma-events/tests/ui/04-event-sanity-check.rs
Normal file
16
ruma-events/tests/ui/04-event-sanity-check.rs
Normal 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() {}
|
8
ruma-events/tests/ui/05-named-fields.rs
Normal file
8
ruma-events/tests/ui/05-named-fields.rs
Normal 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() {}
|
5
ruma-events/tests/ui/05-named-fields.stderr
Normal file
5
ruma-events/tests/ui/05-named-fields.stderr
Normal 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);
|
||||||
|
| ^^^
|
10
ruma-events/tests/ui/06-no-content-field.rs
Normal file
10
ruma-events/tests/ui/06-no-content-field.rs
Normal 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() {}
|
7
ruma-events/tests/ui/06-no-content-field.stderr
Normal file
7
ruma-events/tests/ui/06-no-content-field.stderr
Normal 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)
|
15
ruma-events/tests/ui/07-enum-sanity-check.rs
Normal file
15
ruma-events/tests/ui/07-enum-sanity-check.rs
Normal 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() {}
|
17
ruma-events/tests/ui/08-enum-invalid-path.rs
Normal file
17
ruma-events/tests/ui/08-enum-invalid-path.rs
Normal 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() {}
|
18
ruma-events/tests/ui/08-enum-invalid-path.stderr
Normal file
18
ruma-events/tests/ui/08-enum-invalid-path.stderr
Normal 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`
|
Loading…
x
Reference in New Issue
Block a user