diff --git a/ruma-federation-api/.builds/beta.yml b/ruma-federation-api/.builds/beta.yml new file mode 100644 index 00000000..39aad8ea --- /dev/null +++ b/ruma-federation-api/.builds/beta.yml @@ -0,0 +1,27 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-federation-api +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-federation-api + + # 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 )) diff --git a/ruma-federation-api/.builds/msrv.yml b/ruma-federation-api/.builds/msrv.yml new file mode 100644 index 00000000..191fa3d7 --- /dev/null +++ b/ruma-federation-api/.builds/msrv.yml @@ -0,0 +1,16 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-federation-api +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-federation-api + + # Only make sure the code builds with the MSRV. Tests can require later + # Rust versions, don't compile or run them. + cargo build --verbose diff --git a/ruma-federation-api/.builds/nightly.yml b/ruma-federation-api/.builds/nightly.yml new file mode 100644 index 00000000..76f68ffc --- /dev/null +++ b/ruma-federation-api/.builds/nightly.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-federation-api +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-federation-api + + # 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 )) diff --git a/ruma-federation-api/.builds/stable.yml b/ruma-federation-api/.builds/stable.yml new file mode 100644 index 00000000..c55b377a --- /dev/null +++ b/ruma-federation-api/.builds/stable.yml @@ -0,0 +1,29 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-federation-api +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-federation-api + + # 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 diff --git a/ruma-federation-api/.gitignore b/ruma-federation-api/.gitignore new file mode 100644 index 00000000..fa8d85ac --- /dev/null +++ b/ruma-federation-api/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target diff --git a/ruma-federation-api/CHANGELOG.md b/ruma-federation-api/CHANGELOG.md new file mode 100644 index 00000000..bed78bb5 --- /dev/null +++ b/ruma-federation-api/CHANGELOG.md @@ -0,0 +1,25 @@ +# [unreleased] + +Improvements: + +* Add endpoints: + ``` + directory::get_public_rooms::v1, + discovery::{ + discover_homeserver, + get_server_keys::v2, + get_server_version::v1 + }, + membership::{ + create_join_event::v1, + create_join_event_template::v1 + }, + query::get_room_information::v1, + version::get_server_version::v1 + ``` + +# 0.0.1 + +Improvements: + +* Provide `RoomV3Pdu` type for room versions 3 and above \ No newline at end of file diff --git a/ruma-federation-api/CONTRIBUTING.md b/ruma-federation-api/CONTRIBUTING.md new file mode 100644 index 00000000..bc7c736e --- /dev/null +++ b/ruma-federation-api/CONTRIBUTING.md @@ -0,0 +1,181 @@ +Welcome! Thanks for looking into contributing to our project! + +# Table of Contents + +- [Looking for Help?](#looking-for-help) + - [Documentation](#documentation) + - [Chat Rooms](#chat-rooms) +- [Reporting Issues](#reporting-issues) +- [Submitting Code](#submitting-code) + - [Coding Style](#coding-style) + - [Modifying Endpoints](#modifying-endpoints) + - [Submitting PRs](#submitting-prs) + - [Where do I start?](#where-do-i-start) +- [Testing](#testing) +- [Contact](#contact) + +# Looking for Help? + +Here is a list of helpful resources you can consult: + +## Documentation + +- [Matrix Federation API specification](https://matrix.org/docs/spec/server_server/latest) +- Documentation to other Ruma modules: + - [ruma-events](https://docs.rs/ruma-events/) + - [ruma-api](https://docs.rs/ruma-api/) + - [ruma-client](https://docs.rs/ruma-client/) + +## Chat Rooms + +- Ruma Matrix room: [#ruma:matrix.org](https://matrix.to/#/#ruma:matrix.org) +- Matrix Developer room: [#matrix-dev:matrix.org](https://matrix.to/#/#matrix-dev:matrix.org) +- Matrix Homeserver developers room: [#homeservers-dev:matrix.org](https://matrix.to/#/#homerservers-dev:matrix.org) + +# Reporting Issues + +If you find any bugs, inconsistencies or other problems, feel free to submit +a GitHub [issue](issues). + +If you have a quick question, it may be easier to leave a message on +[#ruma:matrix.org](https://matrix.to/#/#ruma:matrix.org). + +Also, if you have trouble getting on board, let us know so we can help future +contributors to the project overcome that hurdle too. + +# Submitting Code + +Ready to write some code? Great! Here are some guidelines to follow to +help you on your way: + +## Coding Style + +### Import Formatting + +Organize your imports into three groups separated by blank lines: + +1. `std` imports +1. External imports (from other crates) +1. Local imports (`self::`, `super::`, `crate::` and things like `LocalType::*`) + +For example, + +```rust +use std::collections::HashMap; + +use ruma_api::ruma_api; + +use super::MyType; +``` + +Also, group imports by module. For example, do this: + +```rust +use std::{ + collections::HashMap, + convert::TryFrom, + fmt::{Debug, Display, Error as FmtError, Formatter}, +}; +``` + +as opposed to: + +```rust +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::{Debug, Display, Error as FmtError, Formatter}; +``` + +### Code Formatting and Linting + +Use `rustfmt` to format your code and `clippy` to lint your code. Before +committing your changes, go ahead and run `cargo fmt` and `cargo clippy +--all-targets --all-features` on the repository to make sure that the +formatting and linting checks pass in CI. Note that `clippy` warnings are +reported as errors in CI builds, so make sure to handle those before +comitting as well. (To install the tools, run `rustup component add rustfmt +clippy`.) + +#### Git hooks +Tip: You may want to add these commands to your pre-commit git hook so you don't get bitten by CI. + +```sh +# ./git/hooks/pre-commit +cargo fmt && cargo clippy --all-targets --allfeatures +``` + +### Commit Messages + +Write commit messages using the imperative mood, as if completing the sentence: +"If applied, this commit will \_\_\_." For example, use "Fix some bug" instead +of "Fixed some bug" or "Add a feature" instead of "Added a feature". + +(Take a look at this +[blog post](https://www.freecodecamp.org/news/writing-good-commit-messages-a-practical-guide/) +for more information on writing good commit messages.) + +## Modifying Endpoints + +### Matrix Spec Version + +Use the latest r0.x.x documentation when adding or modifying code. We target +the latest minor version of the Matrix specification. (Note: We might +reconsider this when the Federation API hits r1.0.0.) + +### Endpoint Documentation Header + +Add a comment to the top of each endpoint file that includes the path +and a link to the documentation of the spec. You can use the latest +version at the time of the commit. For example: + +```rust +//! [GET /.well-known/matrix/server](https://matrix.org/docs/spec/server_server/r0.1.3#get-well-known-matrix-server) +``` + +### Naming Endpoints + +When adding new endpoints, select the module that fits the purpose of the +endpoint. When naming the endpoint itself, you can use the following +guidelines: +- The name should be a verb describing what the client is requesting, e.g. + `get_some_resource`. +- Endpoints which are basic CRUD operations should use the prefixes + `create`, `get`, `update`, and `delete`. +- The prefix `set` is preferred to create if the resource is a singleton. + In other words, when there's no distinction between `create` and `update`. +- Try to use names that are as descriptive as possible and distinct from + other endpoints in all other modules. (For example, instead of + `membership::create_event::v1`, use `membership::create_join_event::v1`). +- If you're not sure what to name it, pick any name and we can help you + with it. + +### Tracking Changes + +Add your changes to the [change log](CHANGELOG.md). If possible, try to +find and denote the version of the spec that included the change you are +making. + +## Submitting PRs + +Once you're ready to submit your code, create a pull request, and one of our +maintainers will review it. Once your PR has passed review, a maintainer will +merge the request and you're done! 🎉 + +## Where do I start? + +If this is your first contribution to the project, we recommend taking a look +at one of the [open issues][] we've marked for new contributors. + +It may be helpful to peruse some of the documentation for `ruma-events` and +`ruma-api` listed above for some context. + +[open issues]: https://github.com/ruma/ruma-federation-api/issues?q=is%3Aopen+is%3Aissue+label%3Aeffort%2Feasy + +# Testing + +Before committing, run `cargo check` to make sure that your changes can build, as well as running the formatting and linting tools [mentioned above](#code-formatting-and-linting). + +# Contact + +Thanks again for being a contributor! If you have any questions, join us at +[#ruma:matrix.org](https://matrix.to/#/#ruma:matrix.org). diff --git a/ruma-federation-api/Cargo.toml b/ruma-federation-api/Cargo.toml new file mode 100644 index 00000000..6f93144b --- /dev/null +++ b/ruma-federation-api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["Jonas Platte "] +categories = ["api-bindings", "web-programming"] +description = "Types for the endpoints in the Matrix server-server API." +documentation = "https://docs.rs/ruma-federation-api" +edition = "2018" +homepage = "https://github.com/ruma/ruma-federation-api" +keywords = ["matrix", "chat", "messaging", "ruma"] +license = "MIT" +name = "ruma-federation-api" +readme = "README.md" +repository = "https://github.com/ruma/ruma-federation-api" +version = "0.0.2" + +[dependencies] +js_int = "0.1.5" +matches = "0.1.8" +ruma-api = "0.16.1" +ruma-events = "0.21.3" +ruma-identifiers = "0.16.2" +ruma-serde = "0.2.2" +serde = { version = "1.0.111", features = ["derive"] } +serde_json = "1.0.53" diff --git a/ruma-federation-api/LICENSE b/ruma-federation-api/LICENSE new file mode 100644 index 00000000..1ea21301 --- /dev/null +++ b/ruma-federation-api/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 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. diff --git a/ruma-federation-api/README.md b/ruma-federation-api/README.md new file mode 100644 index 00000000..29e4dbc4 --- /dev/null +++ b/ruma-federation-api/README.md @@ -0,0 +1,18 @@ +# ruma-federation-api + +**ruma-federation-api** contains serializable types for the requests and responses for each endpoint in the [Matrix](https://matrix.org/) Federation API specification. +These types can be shared by client and server code. + +## Minimum Rust version + +ruma-federation-api requires Rust 1.40.0 or later. + +## Documentation +[https://docs.rs/ruma-federation-api](https://docs.rs/ruma-federation-api) + +## Contributing +See [CONTRIBUTING.md](CONTRIBUTING.md). + +## License + +[MIT](http://opensource.org/licenses/MIT) diff --git a/ruma-federation-api/src/directory.rs b/ruma-federation-api/src/directory.rs new file mode 100644 index 00000000..d33d3254 --- /dev/null +++ b/ruma-federation-api/src/directory.rs @@ -0,0 +1,3 @@ +//! Room directory endpoints. + +pub mod get_public_rooms; diff --git a/ruma-federation-api/src/directory/get_public_rooms/mod.rs b/ruma-federation-api/src/directory/get_public_rooms/mod.rs new file mode 100644 index 00000000..eb9b0dab --- /dev/null +++ b/ruma-federation-api/src/directory/get_public_rooms/mod.rs @@ -0,0 +1,3 @@ +//! Endpoint to query a homeserver's public rooms. + +pub mod v1; diff --git a/ruma-federation-api/src/directory/get_public_rooms/v1.rs b/ruma-federation-api/src/directory/get_public_rooms/v1.rs new file mode 100644 index 00000000..c989d008 --- /dev/null +++ b/ruma-federation-api/src/directory/get_public_rooms/v1.rs @@ -0,0 +1,177 @@ +//! [GET /_matrix/federation/v1/publicRooms](https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-federation-v1-publicrooms) + +use std::fmt; + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_identifiers::{RoomAliasId, RoomId}; +use serde::{ + de::{MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, +}; + +ruma_api! { + metadata { + description: "Gets all the public rooms for the homeserver.", + method: GET, + name: "get_public_rooms", + path: "/_matrix/federation/v1/publicRooms", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The maximum number of rooms to return. Default is no limit. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + /// Pagination token from a previous request. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub since: Option, + /// Network to fetch the public room lists from. + #[serde(flatten, skip_serializing_if = "ruma_serde::is_default")] + #[ruma_api(query)] + pub room_network: RoomNetwork, + } + + response { + /// A paginated chunk of public rooms. + pub chunk: Vec, + /// A pagination token for the response. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_batch: Option, + /// A pagination token that allows fetching previous results. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + /// An estimate on the total number of public rooms, if the server has an estimate. + pub total_room_count_estimate: Option, + } +} + +/// A chunk of a room list response, describing one room +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PublicRoomsChunk { + /// Aliases of the room. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub aliases: Vec, + /// The canonical alias of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub canonical_alias: Option, + /// The name of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// The number of members joined to the room. + pub num_joined_members: UInt, + /// The ID of the room. + pub room_id: RoomId, + /// The topic of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + /// Whether the room may be viewed by guest users without joining. + pub world_readable: bool, + /// Whether guest users may join the room and participate in it. + /// + /// If they can, they will be subject to ordinary power level rules like any other user. + pub guest_can_join: bool, + /// The URL for the room's avatar, if one is set. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, +} + +/// Information about which networks/protocols from application services on the +/// homeserver from which to request rooms. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RoomNetwork { + /// Return rooms from the Matrix network. + Matrix, + /// Return rooms from all the networks/protocols the homeserver knows about. + All, + /// Return rooms from a specific third party network/protocol. + ThirdParty(String), +} + +impl Default for RoomNetwork { + fn default() -> Self { + RoomNetwork::Matrix + } +} + +impl Serialize for RoomNetwork { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state; + match self { + Self::Matrix => { + state = serializer.serialize_struct("RoomNetwork", 0)?; + } + Self::All => { + state = serializer.serialize_struct("RoomNetwork", 1)?; + state.serialize_field("include_all_networks", &true)?; + } + Self::ThirdParty(network) => { + state = serializer.serialize_struct("RoomNetwork", 1)?; + state.serialize_field("third_party_instance_id", network)?; + } + } + state.end() + } +} + +impl<'de> Deserialize<'de> for RoomNetwork { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(RoomNetworkVisitor) + } +} + +struct RoomNetworkVisitor; +impl<'de> Visitor<'de> for RoomNetworkVisitor { + type Value = RoomNetwork; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Network selection") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut include_all_networks = false; + let mut third_party_instance_id = None; + while let Some((key, value)) = access.next_entry::()? { + match key.as_str() { + "include_all_networks" => { + include_all_networks = match value.as_bool() { + Some(b) => b, + _ => false, + } + } + "third_party_instance_id" => { + third_party_instance_id = value.as_str().map(|v| v.to_owned()) + } + _ => {} + }; + } + + if include_all_networks { + if third_party_instance_id.is_none() { + Ok(RoomNetwork::All) + } else { + Err(M::Error::custom( + "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive.", + )) + } + } else { + Ok(match third_party_instance_id { + Some(network) => RoomNetwork::ThirdParty(network), + None => RoomNetwork::Matrix, + }) + } + } +} diff --git a/ruma-federation-api/src/discovery.rs b/ruma-federation-api/src/discovery.rs new file mode 100644 index 00000000..d09f4583 --- /dev/null +++ b/ruma-federation-api/src/discovery.rs @@ -0,0 +1,5 @@ +//! Server discovery endpoints. + +pub mod discover_homeserver; +pub mod get_server_keys; +pub mod get_server_version; diff --git a/ruma-federation-api/src/discovery/discover_homeserver.rs b/ruma-federation-api/src/discovery/discover_homeserver.rs new file mode 100644 index 00000000..2d6f386d --- /dev/null +++ b/ruma-federation-api/src/discovery/discover_homeserver.rs @@ -0,0 +1,22 @@ +//! [GET /.well-known/matrix/server](https://matrix.org/docs/spec/server_server/r0.1.3#get-well-known-matrix-server) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Get discovery information about the domain.", + method: GET, + name: "discover_homeserver", + path: "/.well-known/matrix/server", + rate_limited: false, + requires_authentication: false, + } + + request {} + + response { + /// The server name to delegate server-server communciations to, with optional port. + #[serde(rename = "m.homeserver")] + pub homeserver: String, + } +} diff --git a/ruma-federation-api/src/discovery/get_server_keys/mod.rs b/ruma-federation-api/src/discovery/get_server_keys/mod.rs new file mode 100644 index 00000000..4de7144b --- /dev/null +++ b/ruma-federation-api/src/discovery/get_server_keys/mod.rs @@ -0,0 +1,3 @@ +//! Endpdoint for retrieving a server's published signing keys. + +pub mod v2; diff --git a/ruma-federation-api/src/discovery/get_server_keys/v2.rs b/ruma-federation-api/src/discovery/get_server_keys/v2.rs new file mode 100644 index 00000000..73160322 --- /dev/null +++ b/ruma-federation-api/src/discovery/get_server_keys/v2.rs @@ -0,0 +1,54 @@ +//! [GET /_matrix/key/v2/server](https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-key-v2-server-keyid) + +use std::{collections::BTreeMap, time::SystemTime}; + +use ruma_api::ruma_api; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Gets the homeserver's published signing keys.", + method: GET, + name: "get_server_keys", + path: "/_matrix/key/v2/server", + rate_limited: false, + requires_authentication: false, + } + + request {} + + response { + // Spec is wrong, all fields are required (see + // https://github.com/matrix-org/matrix-doc/issues/2508) + + /// DNS name of the homeserver. + pub server_name: String, + /// Public keys of the homeserver for verifying digital signatures. + pub verify_keys: BTreeMap, + /// Public keys that the homeserver used to use and when it stopped using them. + pub old_verify_keys: BTreeMap, + /// Digital signatures of this object signed using the verify_keys. + pub signatures: BTreeMap>, + /// Timestamp when the keys should be refreshed. This field MUST be ignored in room + /// versions 1, 2, 3, and 4. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub valid_until_ts: SystemTime, + } +} + +/// Public key of the homeserver for verifying digital signatures. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct VerifyKey { + /// The Unpadded Base64 encoded key. + pub key: String, +} + +/// A key the server used to use, but stopped using. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct OldVerifyKey { + /// Timestamp when this key expired. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub expired_ts: SystemTime, + /// The Unpadded Base64 encoded key. + pub key: String, +} diff --git a/ruma-federation-api/src/discovery/get_server_version/mod.rs b/ruma-federation-api/src/discovery/get_server_version/mod.rs new file mode 100644 index 00000000..35431f43 --- /dev/null +++ b/ruma-federation-api/src/discovery/get_server_version/mod.rs @@ -0,0 +1,3 @@ +//! Endpoint to retrieve metadata about a server implementation. + +pub mod v1; diff --git a/ruma-federation-api/src/discovery/get_server_version/v1.rs b/ruma-federation-api/src/discovery/get_server_version/v1.rs new file mode 100644 index 00000000..ea8f2704 --- /dev/null +++ b/ruma-federation-api/src/discovery/get_server_version/v1.rs @@ -0,0 +1,34 @@ +//! [GET /_matrix/federation/v1/version](https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-federation-v1-version) + +use ruma_api::ruma_api; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Get the implementation name and version of this homeserver.", + method: GET, + name: "discover_homeserver", + path: "/.well-known/matrix/server", + rate_limited: false, + requires_authentication: false, + } + + request {} + + response { + /// Information about the homeserver implementation + #[serde(skip_serializing_if = "Option::is_none")] + pub server: Option, + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +/// Arbitrary values that identify this implementation. +pub struct Server { + /// Arbitrary name that identifies this implementation. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Version of this implementation. The version format depends on the implementation. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} diff --git a/ruma-federation-api/src/lib.rs b/ruma-federation-api/src/lib.rs new file mode 100644 index 00000000..ef6a1c66 --- /dev/null +++ b/ruma-federation-api/src/lib.rs @@ -0,0 +1,70 @@ +//! (De)serializable types for the Matrix Federation API. + +#![warn(missing_docs)] + +use std::{collections::BTreeMap, time::SystemTime}; + +use ::serde::{Deserialize, Serialize}; +use js_int::UInt; +use ruma_events::EventType; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde_json::Value as JsonValue; + +mod serde; + +pub mod directory; +pub mod discovery; +pub mod membership; +pub mod query; + +/// A 'persistent data unit' (event) for room versions 3 and beyond. +#[derive(Deserialize, Serialize)] +pub struct RoomV3Pdu { + /// The room this event belongs to. + pub room_id: RoomId, + /// The user id of the user who sent this event. + pub sender: UserId, + /// The `server_name` of the homeserver that created this event. + pub origin: String, + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver + /// of when this event was created. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + // TODO: Replace with event content collection from ruma-events once that exists + /// The event's type. + #[serde(rename = "type")] + pub kind: EventType, + /// The event's content. + pub content: JsonValue, + + /// A key that determines which piece of room state the event represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_key: Option, + /// Event IDs for the most recent events in the room that the homeserver was + /// aware of when it created this event. + pub prev_events: Vec, + /// The maximum depth of the `prev_events`, plus one. + pub depth: UInt, + /// Event IDs for the authorization events that would allow this event to be + /// in the room. + pub auth_events: Vec, + /// For redaction events, the ID of the event being redacted. + #[serde(skip_serializing_if = "Option::is_none")] + pub redacts: Option, + /// Additional data added by the origin server but not covered by the + /// signatures. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unsigned: BTreeMap, + /// Content hashes of the PDU. + pub hashes: EventHash, + /// Signatures for the PDU. + pub signatures: BTreeMap>, +} + +/// Content hashes of a PDU. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EventHash { + /// The SHA-256 hash. + pub sha256: String, +} diff --git a/ruma-federation-api/src/membership.rs b/ruma-federation-api/src/membership.rs new file mode 100644 index 00000000..6ca64dd8 --- /dev/null +++ b/ruma-federation-api/src/membership.rs @@ -0,0 +1,4 @@ +//! Room membership endpoints. + +pub mod create_join_event; +pub mod create_join_event_template; diff --git a/ruma-federation-api/src/membership/create_join_event/mod.rs b/ruma-federation-api/src/membership/create_join_event/mod.rs new file mode 100644 index 00000000..4c6d2730 --- /dev/null +++ b/ruma-federation-api/src/membership/create_join_event/mod.rs @@ -0,0 +1,20 @@ +//! Endpoint to send join events to remote homeservers. + +pub mod v1; + +use ruma_events::EventJson; +use serde::{Deserialize, Serialize}; + +use crate::RoomV3Pdu; + +/// Full state of the room. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomState { + /// The resident server's DNS name. + pub origin: String, + /// The full set of authorization events that make up the state of the room, + /// and their authorization events, recursively. + pub auth_chain: Vec>, + /// The room state. + pub state: Vec>, +} diff --git a/ruma-federation-api/src/membership/create_join_event/v1.rs b/ruma-federation-api/src/membership/create_join_event/v1.rs new file mode 100644 index 00000000..e4403f04 --- /dev/null +++ b/ruma-federation-api/src/membership/create_join_event/v1.rs @@ -0,0 +1,104 @@ +//! [PUT /_matrix/federation/v1/send_join/{roomId}/{eventId}](https://matrix.org/docs/spec/server_server/r0.1.3#put-matrix-federation-v1-send-join-roomid-eventid) + +use std::{collections::BTreeMap, time::SystemTime}; + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde_json::Value as JsonValue; + +use super::RoomState; +use crate::{EventHash, RoomV3Pdu}; + +ruma_api! { + metadata { + description: "Send a join event to a resident server.", + name: "create_join_event", + method: PUT, + path: "/_matrix/federation/v1/send_join/:room_id/:event_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room ID that is about to be joined. + #[ruma_api(path)] + pub room_id: RoomId, + /// The user ID the join event will be for. + #[ruma_api(path)] + pub event_id: EventId, + + /// The user id of the user who sent this event. + pub sender: UserId, + /// The `server_name` of the homeserver that created this event. + pub origin: String, + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver + /// of when this event was created. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + // TODO: Replace with event content collection from ruma-events once that exists + /// The event's type. + #[serde(rename = "type")] + pub kind: EventType, + /// The event's content. + pub content: JsonValue, + + /// A key that determines which piece of room state the event represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_key: Option, + /// Event IDs for the most recent events in the room that the homeserver was + /// aware of when it created this event. + pub prev_events: Vec, + /// The maximum depth of the `prev_events`, plus one. + pub depth: UInt, + /// Event IDs for the authorization events that would allow this event to be + /// in the room. + pub auth_events: Vec, + /// For redaction events, the ID of the event being redacted. + #[serde(skip_serializing_if = "Option::is_none")] + pub redacts: Option, + /// Additional data added by the origin server but not covered by the + /// signatures. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unsigned: BTreeMap, + /// Content hashes of the PDU. + pub hashes: EventHash, + /// Signatures for the PDU. + pub signatures: BTreeMap>, + } + + response { + /// Full state of the room. + #[ruma_api(body)] + #[serde(with = "crate::serde::room_state")] + pub room_state: RoomState, + } +} + +impl Request { + /// Helper method to get event ID and PDU (with room ID) from the request + /// parameters. + pub fn into_id_and_v3_pdu(self) -> (EventId, RoomV3Pdu) { + ( + self.event_id, + RoomV3Pdu { + room_id: self.room_id, + sender: self.sender, + origin: self.origin, + origin_server_ts: self.origin_server_ts, + kind: self.kind, + content: self.content, + state_key: self.state_key, + prev_events: self.prev_events, + depth: self.depth, + auth_events: self.auth_events, + redacts: self.redacts, + unsigned: self.unsigned, + hashes: self.hashes, + signatures: self.signatures, + }, + ) + } +} diff --git a/ruma-federation-api/src/membership/create_join_event_template/mod.rs b/ruma-federation-api/src/membership/create_join_event_template/mod.rs new file mode 100644 index 00000000..1007e5ba --- /dev/null +++ b/ruma-federation-api/src/membership/create_join_event_template/mod.rs @@ -0,0 +1,3 @@ +//! Endpoint to request a template for join events. + +pub mod v1; diff --git a/ruma-federation-api/src/membership/create_join_event_template/v1.rs b/ruma-federation-api/src/membership/create_join_event_template/v1.rs new file mode 100644 index 00000000..a25af1fd --- /dev/null +++ b/ruma-federation-api/src/membership/create_join_event_template/v1.rs @@ -0,0 +1,39 @@ +//! [GET /_matrix/federation/v1/make_join/{roomId}/{userId}](https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-federation-v1-make-join-roomid-userid) + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::EventJson; +use ruma_identifiers::{RoomId, UserId}; + +use crate::RoomV3Pdu; + +ruma_api! { + metadata { + description: "Send a request for a join event template to a resident server.", + name: "create_join_event_template", + method: GET, + path: "/_matrix/federation/v1/make_join/:room_id/:user_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room ID that is about to be joined. + #[ruma_api(path)] + pub room_id: RoomId, + /// The user ID the join event will be for. + #[ruma_api(path)] + pub user_id: UserId, + #[ruma_api(query)] + /// The room versions the sending server has support for. Defaults to [1]. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub ver: Vec, + } + + response { + /// The version of the room where the server is trying to join. + pub room_version: Option, + /// An unsigned template event. + pub event: EventJson, + } +} diff --git a/ruma-federation-api/src/query.rs b/ruma-federation-api/src/query.rs new file mode 100644 index 00000000..7dc27cdb --- /dev/null +++ b/ruma-federation-api/src/query.rs @@ -0,0 +1,3 @@ +//! Endpoints to retrieve information from a homeserver about a resource. + +pub mod get_room_information; diff --git a/ruma-federation-api/src/query/get_room_information/mod.rs b/ruma-federation-api/src/query/get_room_information/mod.rs new file mode 100644 index 00000000..bf4d11f2 --- /dev/null +++ b/ruma-federation-api/src/query/get_room_information/mod.rs @@ -0,0 +1,2 @@ +//! Endpoint to query room information with a room alias. +pub mod v1; diff --git a/ruma-federation-api/src/query/get_room_information/v1.rs b/ruma-federation-api/src/query/get_room_information/v1.rs new file mode 100644 index 00000000..77c7219a --- /dev/null +++ b/ruma-federation-api/src/query/get_room_information/v1.rs @@ -0,0 +1,28 @@ +//! [GET /_matrix/federation/v1/query/directory](https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-federation-v1-query-directory) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +ruma_api! { + metadata { + description: "Get mapped room ID and resident homeservers for a given room alias.", + name: "get_room_information", + method: GET, + path: "/_matrix/federation/v1/query/directory", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Room alias to query. + #[ruma_api(query)] + pub room_alias: String, + } + + response { + /// Room ID mapped to queried alias. + pub room_id: RoomId, + /// An array of server names that are likely to hold the given room. + pub servers: Vec, + } +} diff --git a/ruma-federation-api/src/serde.rs b/ruma-federation-api/src/serde.rs new file mode 100644 index 00000000..3808d8c5 --- /dev/null +++ b/ruma-federation-api/src/serde.rs @@ -0,0 +1,3 @@ +//! Modules for custom serde de/-serialization implementations. + +pub mod room_state; diff --git a/ruma-federation-api/src/serde/room_state.rs b/ruma-federation-api/src/serde/room_state.rs new file mode 100644 index 00000000..6df5933b --- /dev/null +++ b/ruma-federation-api/src/serde/room_state.rs @@ -0,0 +1,150 @@ +//! A module to deserialize a RoomState struct from incorrectly specified v1 +//! send_join endpoint. +//! +//! For more information, see this [GitHub issue](https://github.com/matrix-org/matrix-doc/issues/2541). + +use std::fmt; + +use serde::{ + de::{Deserializer, Error, IgnoredAny, SeqAccess, Visitor}, + ser::{SerializeSeq, Serializer}, +}; + +use crate::membership::create_join_event::RoomState; + +pub fn serialize(room_state: &RoomState, serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&200)?; + seq.serialize_element(room_state)?; + seq.end() +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(RoomStateVisitor) +} + +struct RoomStateVisitor; + +impl<'de> Visitor<'de> for RoomStateVisitor { + type Value = RoomState; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Room State response wrapped in an array.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let expected = "a two-element list in the response"; + if seq.next_element::()?.is_none() { + return Err(A::Error::invalid_length(0, &expected)); + } + + let room_state = seq + .next_element()? + .ok_or_else(|| A::Error::invalid_length(1, &expected))?; + + while let Some(IgnoredAny) = seq.next_element()? { + // ignore extra elements + } + + Ok(room_state) + } +} + +#[cfg(test)] +mod tests { + use matches::assert_matches; + use serde_json::{json, to_value as to_json_value}; + + use super::{deserialize, serialize, RoomState}; + + #[test] + fn test_deserialize_response() { + let response = json!([ + 200, + { + "origin": "example.com", + "auth_chain": [], + "state": [] + } + ]); + + let parsed = deserialize(response).unwrap(); + + assert_matches!( + parsed, + RoomState { origin, auth_chain, state } + if origin == "example.com" + && auth_chain.is_empty() + && state.is_empty() + ); + } + + #[test] + fn test_serialize_response() { + let room_state = RoomState { + origin: "matrix.org".to_string(), + auth_chain: Vec::new(), + state: Vec::new(), + }; + + let serialized = serialize(&room_state, serde_json::value::Serializer).unwrap(); + let expected = to_json_value(&json!( + [ + 200, + { + "origin": "matrix.org", + "auth_chain": [], + "state": [] + } + ] + )) + .unwrap(); + + assert_eq!(serialized, expected); + } + + #[test] + fn test_too_short_array() { + let json = json!([200]); + let failed_room_state = deserialize(json); + assert_eq!( + failed_room_state.unwrap_err().to_string(), + "invalid length 1, expected a two-element list in the response" + ); + } + + #[test] + fn test_not_an_array() { + let json = json!({ + "origin": "matrix.org", + "auth_chain": [], + "state": [] + }); + let failed_room_state = deserialize(json); + + assert_eq!( + failed_room_state.unwrap_err().to_string(), + "invalid type: map, expected Room State response wrapped in an array.", + ) + } + + #[test] + fn test_too_long_array() { + let json = json!([200, {"origin": "", "auth_chain": [], "state": []}, 200]); + assert_matches!( + deserialize(json).unwrap(), + RoomState { origin, auth_chain, state } + if origin == "" + && auth_chain.is_empty() + && state.is_empty() + ); + } +}