diff --git a/ruma-client-api/.builds/beta.yml b/ruma-client-api/.builds/beta.yml new file mode 100644 index 00000000..4a0abed4 --- /dev/null +++ b/ruma-client-api/.builds/beta.yml @@ -0,0 +1,30 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client-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-client-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 --no-default-features --verbose + test1_exit=$? + + cargo test --all-features --verbose + test2_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test1_exit || $test2_exit )) diff --git a/ruma-client-api/.builds/msrv.yml b/ruma-client-api/.builds/msrv.yml new file mode 100644 index 00000000..5f118e3f --- /dev/null +++ b/ruma-client-api/.builds/msrv.yml @@ -0,0 +1,26 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client-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-client-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 + + # Only make sure the code builds with the MSRV. Tests can require later + # Rust versions, don't compile or run them. + cargo build --no-default-features --verbose + build1_exit=$? + + cargo build --all-features --verbose + build2_exit=$? + + exit $(( $build1_exit || $build2_exit )) diff --git a/ruma-client-api/.builds/nightly.yml b/ruma-client-api/.builds/nightly.yml new file mode 100644 index 00000000..bf523c21 --- /dev/null +++ b/ruma-client-api/.builds/nightly.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client-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-client-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-client-api/.builds/stable.yml b/ruma-client-api/.builds/stable.yml new file mode 100644 index 00000000..73ba28ed --- /dev/null +++ b/ruma-client-api/.builds/stable.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client-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-client-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 --no-default-features --verbose + test1_exit=$? + + cargo test --all-features --verbose + test2_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test1_exit || $test2_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-client-api/.gitignore b/ruma-client-api/.gitignore new file mode 100644 index 00000000..fa8d85ac --- /dev/null +++ b/ruma-client-api/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target diff --git a/ruma-client-api/CHANGELOG.md b/ruma-client-api/CHANGELOG.md new file mode 100644 index 00000000..5bfc9d9b --- /dev/null +++ b/ruma-client-api/CHANGELOG.md @@ -0,0 +1,202 @@ +# [unreleased] + +Breaking changes: + +* Make `avatar_url` in `r0::profile::set_avatar_url::Request` an `Option` +* Update type of `canonical_alias` in `r0::directory::PublicRoomsChunk` from + `Option` to `Option` +* Update `r0::room::create_room::CreationContent` + * Change `federated`s type from `Option` to `bool` + * Add `predecessor` field +* Update `r0::push::get_pushrules_all` and `r0::push::get_pushrules_global_scope` to use the + `Ruleset` type from `ruma_events` +* Fix event types in `r0::context::get_context` + +Improvements: + +* Add method `into_event_content` for `r0::room::create_room::CreationContent` +* Add room visibility endpoints: `r0::directory::{get_room_visibility, set_room_visibility}`. + +Deprecations: + +* `r0::sync::sync_events::SetPresence` has been renamed to `PresenceState`. It is still available + under its previous name, but only for one release. + +# 0.9.0 + +Bug fixes: + +* Fix (de)serialization for `r0::media::get_content_thumnail::Response` +* Make `r0::device::get_devices::Response::devices` public + +Breaking changes: + +* The `event_id` in the response for the message and state sending endpoints is now required + * r0.6.0 doesn't say they are required, but this has been fixed for the next version of the spec +* Updated the type of `r0::sync::sync_events::DeviceLists` fields +* Change `r0::device::Device` fields according to the spec + +Improvements: + +* `r0::keys::AlgorithmAndDeviceId` now implements `Display` + +# 0.8.0 + +Breaking changes: + +* Update all endpoints to r0.6.0 + * Some of the changes from that might not be listed below, but it should be + easy to figure out what changed from the documentation and compiler errors + if you are using any of the affected endpoints. +* Add `server_name` parameter to `r0::join::join_room_by_id_or_alias` +* Modify `r0::account::AuthenticationData`: + * Rename to `AuthData` + * Change to an enum to facilitate fallback auth acknowledgements + * Add `auth_parameters` field + * Move to `r0::uiaa` module +* Add `room_network` parameter to `r0::directory::get_public_rooms_filtered` to + represent `include_all_networks` and `third_party_instance_id` Matrix fields +* Update `r0::account::register` endpoint: + * Remove `bind_email` request field (removed in r0.6.0) + * Remove `inhibit_login` request field, make `access_token` and `device_id` response fields optional (added in r0.4.0) + * Remove deprecated `home_server` response field (removed in r0.4.0) +* Update `r0::contact::get_contacts` endpoint to r0.6.0 +* Change `UInt` timestamps to `SystemTime` in: + * `media::get_media_preview::Request` + * `push::get_notifications::Notification` + * `server::get_user_info::ConnectionInfo` + * `device::Device` +* Change all usages of `HashMap` to `BTreeMap` +* Change the messages type that gets sent out using the `r0::client_exchange::send_event_to_device` + request. +* Add `M_USER_DEACTIVATED` to `error::ErrorKind` +* Make `display_name` field of `r0::membership::joined_events::RoomMember` optional +* Update `r0::search::search_events` to r0.6.0 +* Add `account_data` field to `r0::sync::sync_events` +* Rename `r0::client_exchange` to `r0::to_device` + +Improvements: + +* Add types for User-Interactive Authentication API: `r0::uiaa::{AuthFlow, UiaaInfo, UiaaResponse}` +* Add missing serde attributes to `get_content_thumbnail` query parameters +* Add missing `state` response field to `r0::message::get_message_events` +* Normalize `serde_json` imports +* Remove dependeny on the `url` crate + +# 0.7.2 + +Bug fixes: + +* Fix `create_room` requests without an `initial_state` field failing deserialization +* Fix `sync_events` responses without a `device_one_time_keys_count` field failing deserialization + +# 0.7.1 + +Bug fixes: + +* Fix deserialization of `sync_events::Request` +* Fix (de)serialization of `sync_events::RoomSummary` + +# 0.7.0 + +Breaking changes: + +* Update ruma-api to 0.15.0 +* Update ruma-events to 0.18.0 +* Fix `r0::session::get_login_types` +* Add `allow_remote` parameter to `r0::media::get_content` +* Add missing parameters for `r0::room::create_room` +* Moved `r0::room::create_room::Invite3pid` to `r0::membership::Invite3pid` +* Replaced `user_id` parameter of `r0::membership::invite_user` with `recipient` + to allow invitation of users by either Matrix or third party identifiers. +* Remove deprecated endpoint `r0::contact::create_contact` (deprecated in r0.6.0) +* Add lazy-loading options to `r0::filter::RoomEventFilter` (introduced in r0.5.0) +* Change type for `limit` request parameter of `r0::context::get_context` from `u8` to `Option` +* Use `std::time::Duration` for appropriate fields on several endpoints: + ``` + r0::{ + account::request_openid_token, + keys::{claim_keys, get_keys}, + presence::get_presence, + sync::sync_events, + typing::create_typing_event, + voip::get_turn_server_info + } + ``` + +Improvements: + +* Add an `Error` type that represents the well-known errors in the client-server API + * the response deserialization code will try to create an instance of this type from http responses that indicate an error +* Add OpenID token request endpoint. +* Add `r0::client_exchange::send_event_to_device` (introduced in r0.3.0) +* Add endpoints to retrieve account_data (introduced in r0.5.0) +* Add media endpoints: `r0::media::{get_media_config, get_media_preview, get_content_as_filename}` +* Add `unstable_features` to `unversioned::get_supported_versions` (introduced in r0.5.0) +* Add request and response parameters for `r0::account::deactivate` +* Add `r0::session::sso_login` (introduced in r0.5.0) +* Add `filter` type for `r0::context::get_context` + +# 0.6.0 + +Breaking changes: + +* Update ruma-api to 0.13.0 +* Our Minimum Supported Rust Version is now 1.40.0 +* Remove presence list endpoints `r0::presence::{get_subscribed_presences, update_presence_subscriptions}` (removed in r0.5.0) +* Refactor `r0::send` endpoints and remove module: + * Move `r0::send::send_message_event` to `r0::message::create_message_event` + * Move `r0::send::send_state_event_for_empty_key` to `r0::state:create_state_event_for_empty_key` + * Move `r0::send::send_state_event_for_key` to `r0::state:create_state_event_for_key` +* Refactor `r0::sync` endpoints: + * Move `r0::sync::get_member_events` to `r0::membership::get_member_events` + * Move `r0::sync::get_message_events` to `r0::message::get_message_events` + * Move `r0::sync::get_state_events` to `r0::state::get_state_events` + * Move `r0::sync::get_state_events_for_empty_key` to `r0::state::get_state_events_for_empty_key` + * Move `r0::sync::get_state_events_for_key` to `r0::state::get_state_events_for_key` +* Update endpoints for requesting account management tokens via email: + * Move `r0::account::request_password_change_token` to `r0::account::request_password_change_token_via_email` + * Move `r0::account::request_register_token` to `r0::account::request_registration_token_via_email` + * Modify `r0::account::request_registration_token_via_email` not to be rate-limited and require authentication +* Merge duplicate enums `r0::contact::get_contact::Medium` and `r0::session::login::Medium` and move them to `r0::thirdparty` + +Improvements: + +* Add `r0::device` endpoints +* Add `r0::room::get_room_event` (introduced in r0.4.0) +* Add `r0::read_marker::set_read_marker` (introduced in r0.4.0) +* Add `r0::capabilities::get_capabilities` (introduced in r0.5.0) +* Add `r0::keys` endpoints (introduced in r0.3.0) +* Add `r0::session::get_login_types` (introduced in r0.4.0) +* Add `r0::account::get_username_availability` (introduced in r0.4.0) +* Add endpoints to request management tokens (introduced upstream in r0.4.0): + * `r0::account::request_3pid_management_token_via_msisdn` + * `r0::account::request_password_change_token_via_msisdn` + * `r0::account::request_registration_token_via_msisdn` + * `r0::acount::request_3pid_management_token_via_email` +* Update `r0::presence_get_presence` from r0.4.0 to r0.6.0 +* Add `r0::account::bind_3pid` +* Add `r0::account::delete_3pid` +* Add `r0::account::unbind_3pid` +* Add `r0::push` endpoints +* Add `r0::room::upgrade_room` (introduced upstream in r0.5.0) + +# 0.5.0 + +Breaking changes: + +* Our Minimum Supported Rust Version is now 1.39.0 +* Update ruma-api from 0.11.0 to 0.12.0 +* Move `r0::directory::get_public_rooms::PublicRoomsChunk` to `r0::directory::PublicRoomsChunk` +* Move `r0::room::create_room::Visibility` to `r0::room::Visibility` +* Move `r0::account::register::AuthenticationData` to `r0::account::AuthenticationData` + +Improvements: + +* Update `r0::directory::get_public_rooms` from r0.3.0 to r0.6.0 +* Add `r0::directory::get_public_rooms_filtered` (introduced upstream in r0.3.0) +* Add `filter` optional parameter to `r0::sync::get_message_events` (introduced upstream in r0.3.0) +* Add `r0::appservice::set_room_visibility` (part of application service extensions for the client-server API) +* Add `contains_url` to `r0::filter::RoomEventFilter` (introduced upstream in r0.3.0) +* Update `r0::account::change_password` from r0.3.0 to r0.6.0 + * Add optional `auth` field diff --git a/ruma-client-api/CONTRIBUTING.md b/ruma-client-api/CONTRIBUTING.md new file mode 100644 index 00000000..5e98c4ff --- /dev/null +++ b/ruma-client-api/CONTRIBUTING.md @@ -0,0 +1,203 @@ +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 spec Documentation](https://matrix.org/docs/spec/client_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) + +# 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 + +### Common types + +When writing endpoint definitions, use the following mapping from request / +response field types listed in the specification to Rust types: + +Specification type | Rust type +-------------------|--------------------------------------------------------------------------------------------------------------------- +`boolean` | `bool` +`integer` | `js_int::UInt` (unless denoted as signed, then `js_int::Int`) +`string` | If for an identifier (e.g. user ID, room ID), use one of the types from `ruma-identifiers`. Otherwise, use `String`. +`object` | `serde_json::Value` +`[…]` | `Vec<…>` +`{string: …}` | `BTreeMap` (or `BTreeMap`) + +### 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::BTreeMap; + +use ruma_api::ruma_api; + +use super::MyType; +``` + +Also, group imports by module. For example, do this: + +```rust +use std::{ + collections::BTreeMap, + convert::TryFrom, + fmt::{self, Debug, Display, Formatter}, +}; +``` + +as opposed to: + +```rust +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::fmt::{self, Debug, Display, Formatter}; +``` + +### Serde Imports + +When importing methods and types from `serde_json`, methods should be such as +`serde_json::{from,to}_{slice,string,value,vec}` should be imported as +`{from,to}_json_{slice,string,value,vec}`. + +For example: + +```rust +use serde_json::{ + from_value as from_json_value, + to_str as to_json_str, +}; +``` + +Also, `serde_json::Value` should be imported as `JsonValue`. + +### 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`.) + +### 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 Client-Server 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/client](https://matrix.org/docs/spec/client_server/r0.4.0#get-well-known-matrix-client) +``` + +### 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 + `r0::room::get_event`, use `r0::room::get_room_event`). +- 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-client-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-client-api/Cargo.toml b/ruma-client-api/Cargo.toml new file mode 100644 index 00000000..17e4ce78 --- /dev/null +++ b/ruma-client-api/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors = [ + "Jimmy Cuadra ", + "Jonas Platte ", + "Isaiah Inuwa ", +] +categories = ["api-bindings", "web-programming"] +description = "Types for the endpoints in the Matrix client-server API." +documentation = "https://docs.rs/ruma-client-api" +homepage = "https://github.com/ruma/ruma-client-api" +keywords = ["matrix", "chat", "messaging", "ruma"] +license = "MIT" +name = "ruma-client-api" +readme = "README.md" +repository = "https://github.com/ruma/ruma-client-api" +version = "0.9.0" +edition = "2018" + +[dependencies] +http = "0.2.1" +js_int = { version = "0.1.5", features = ["serde"] } +ruma-api = "0.16.1" +ruma-common = "0.1.3" +ruma-events = { git = "https://github.com/ruma/ruma-events", rev = "c1ee72d" } +ruma-identifiers = "0.16.2" +ruma-serde = "0.2.2" +serde = { version = "1.0.111", features = ["derive"] } +serde_json = "1.0.53" +strum = { version = "0.18.0", features = ["derive"] } + +[dev-dependencies] +maplit = "1.0.2" +matches = "0.1.8" diff --git a/ruma-client-api/LICENSE b/ruma-client-api/LICENSE new file mode 100644 index 00000000..4d376442 --- /dev/null +++ b/ruma-client-api/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 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-client-api/README.md b/ruma-client-api/README.md new file mode 100644 index 00000000..a58f0978 --- /dev/null +++ b/ruma-client-api/README.md @@ -0,0 +1,17 @@ +# ruma-client-api + +[![crates.io page](https://img.shields.io/crates/v/ruma-client-api.svg)](https://crates.io/crates/ruma-client-api) +[![docs.rs page](https://docs.rs/ruma-client-api/badge.svg)](https://docs.rs/ruma-client-api/) +[![build status](https://travis-ci.org/ruma/ruma-client-api.svg?branch=master)](https://travis-ci.org/ruma/ruma-client-api) +![license: MIT](https://img.shields.io/crates/l/ruma-client-api.svg) + +**ruma-client-api** contains serializable types for the requests and responses for each endpoint in the [Matrix](https://matrix.org/) client API specification. +These types can be shared by client and server code. + +## Minimum Rust version + +ruma-client-api requires Rust 1.40.0 or later. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/ruma-client-api/src/error.rs b/ruma-client-api/src/error.rs new file mode 100644 index 00000000..fb939caa --- /dev/null +++ b/ruma-client-api/src/error.rs @@ -0,0 +1,245 @@ +//! Errors that can be sent from the homeserver. + +use std::fmt::{self, Display, Formatter}; + +use ruma_api::{error::ResponseDeserializationError, EndpointError}; +use serde::{Deserialize, Serialize}; +use serde_json::{from_slice as from_json_slice, to_vec as to_json_vec}; +use strum::{AsRefStr, Display, EnumString}; + +/// An enum for the error kind. Items may contain additional information. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, AsRefStr, Display, EnumString)] +#[serde(tag = "errcode")] +#[cfg_attr(test, derive(PartialEq))] +pub enum ErrorKind { + /// M_FORBIDDEN + #[serde(rename = "M_FORBIDDEN")] + #[strum(to_string = "M_FORBIDDEN")] + Forbidden, + + /// M_UNKNOWN_TOKEN + #[serde(rename = "M_UNKNOWN_TOKEN")] + #[strum(to_string = "M_UNKNOWN_TOKEN")] + UnknownToken, + + /// M_MISSING_TOKEN + #[serde(rename = "M_MISSING_TOKEN")] + #[strum(to_string = "M_MISSING_TOKEN")] + MissingToken, + + /// M_BAD_JSON + #[serde(rename = "M_BAD_JSON")] + #[strum(to_string = "M_BAD_JSON")] + BadJson, + + /// M_NOT_JSON + #[serde(rename = "M_NOT_JSON")] + #[strum(to_string = "M_NOT_JSON")] + NotJson, + + /// M_NOT_FOUND + #[serde(rename = "M_NOT_FOUND")] + #[strum(to_string = "M_NOT_FOUND")] + NotFound, + + /// M_LIMIT_EXCEEDED + #[serde(rename = "M_LIMIT_EXCEEDED")] + #[strum(to_string = "M_LIMIT_EXCEEDED")] + LimitExceeded, + + /// M_UNKNOWN + #[serde(rename = "M_UNKNOWN")] + #[strum(to_string = "M_UNKNOWN")] + Unknown, + + /// M_UNRECOGNIZED + #[serde(rename = "M_UNRECOGNIZED")] + #[strum(to_string = "M_UNRECOGNIZED")] + Unrecognized, + + /// M_UNAUTHORIZED + #[serde(rename = "M_UNAUTHORIZED")] + #[strum(to_string = "M_UNAUTHORIZED")] + Unauthorized, + + /// M_USER_DEACTIVATED + #[serde(rename = "M_USER_DEACTIVATED")] + #[strum(to_string = "M_USER_DEACTIVATED")] + UserDeactivated, + + /// M_USER_IN_USE + #[serde(rename = "M_USER_IN_USE")] + #[strum(to_string = "M_USER_IN_USE")] + UserInUse, + + /// M_INVALID_USERNAME + #[serde(rename = "M_INVALID_USERNAME")] + #[strum(to_string = "M_INVALID_USERNAME")] + InvalidUsername, + + /// M_ROOM_IN_USE + #[serde(rename = "M_ROOM_IN_USE")] + #[strum(to_string = "M_ROOM_IN_USE")] + RoomInUse, + + /// M_INVALID_ROOM_STATE + #[serde(rename = "M_INVALID_ROOM_STATE")] + #[strum(to_string = "M_INVALID_ROOM_STATE")] + InvalidRoomState, + + /// M_THREEPID_IN_USE + #[serde(rename = "M_THREEPID_IN_USE")] + #[strum(to_string = "M_THREEPID_IN_USE")] + ThreepidInUse, + + /// M_THREEPID_NOT_FOUND + #[serde(rename = "M_THREEPID_NOT_FOUND")] + #[strum(to_string = "M_THREEPID_NOT_FOUND")] + ThreepidNotFound, + + /// M_THREEPID_AUTH_FAILED + #[serde(rename = "M_THREEPID_AUTH_FAILED")] + #[strum(to_string = "M_THREEPID_AUTH_FAILED")] + ThreepidAuthFailed, + + /// M_THREEPID_DENIED + #[serde(rename = "M_THREEPID_DENIED")] + #[strum(to_string = "M_THREEPID_DENIED")] + ThreepidDenied, + + /// M_SERVER_NOT_TRUSTED + #[serde(rename = "M_SERVER_NOT_TRUSTED")] + #[strum(to_string = "M_SERVER_NOT_TRUSTED")] + ServerNotTrusted, + + /// M_UNSUPPORTED_ROOM_VERSION + #[serde(rename = "M_UNSUPPORTED_ROOM_VERSION")] + #[strum(to_string = "M_UNSUPPORTED_ROOM_VERSION")] + UnsupportedRoomVersion, + + /// M_INCOMPATIBLE_ROOM_VERSION + #[serde(rename = "M_INCOMPATIBLE_ROOM_VERSION")] + #[strum(to_string = "M_INCOMPATIBLE_ROOM_VERSION")] + IncompatibleRoomVersion, + + /// M_BAD_STATE + #[serde(rename = "M_BAD_STATE")] + #[strum(to_string = "M_BAD_STATE")] + BadState, + + /// M_GUEST_ACCESS_FORBIDDEN + #[serde(rename = "M_GUEST_ACCESS_FORBIDDEN")] + #[strum(to_string = "M_GUEST_ACCESS_FORBIDDEN")] + GuestAccessForbidden, + + /// M_CAPTCHA_NEEDED + #[serde(rename = "M_CAPTCHA_NEEDED")] + #[strum(to_string = "M_CAPTCHA_NEEDED")] + CaptchaNeeded, + + /// M_CAPTCHA_INVALID + #[serde(rename = "M_CAPTCHA_INVALID")] + #[strum(to_string = "M_CAPTCHA_INVALID")] + CaptchaInvalid, + + /// M_MISSING_PARAM + #[serde(rename = "M_MISSING_PARAM")] + #[strum(to_string = "M_MISSING_PARAM")] + MissingParam, + + /// M_INVALID_PARAM + #[serde(rename = "M_INVALID_PARAM")] + #[strum(to_string = "M_INVALID_PARAM")] + InvalidParam, + + /// M_TOO_LARGE + #[serde(rename = "M_TOO_LARGE")] + #[strum(to_string = "M_TOO_LARGE")] + TooLarge, + + /// M_EXCLUSIVE + #[serde(rename = "M_EXCLUSIVE")] + #[strum(to_string = "M_EXCLUSIVE")] + Exclusive, +} + +/// A Matrix Error without a status code +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ErrorBody { + /// A value which can be used to handle an error message + #[serde(flatten)] + pub kind: ErrorKind, + + /// A human-readable error message, usually a sentence explaining what went wrong. + #[serde(rename = "error")] + pub message: String, +} + +/// A Matrix Error +#[derive(Debug, Clone)] +pub struct Error { + /// A value which can be used to handle an error message + pub kind: ErrorKind, + + /// A human-readable error message, usually a sentence explaining what went wrong. + pub message: String, + + /// The http status code + pub status_code: http::StatusCode, +} + +impl EndpointError for Error { + fn try_from_response( + response: http::Response>, + ) -> Result { + match from_json_slice::(response.body()) { + Ok(error_body) => Ok(error_body.into_error(response.status())), + Err(de_error) => Err(ResponseDeserializationError::new(de_error, response)), + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "[{} / {}] {}", + self.status_code.as_u16(), + self.kind, + self.message + ) + } +} + +impl std::error::Error for Error {} + +impl From for ErrorBody { + fn from(error: Error) -> Self { + Self { + kind: error.kind, + message: error.message, + } + } +} + +impl ErrorBody { + /// Convert the ErrorBody into an Error by adding the http status code. + pub fn into_error(self, status_code: http::StatusCode) -> Error { + Error { + kind: self.kind, + message: self.message, + status_code, + } + } +} + +impl From for http::Response> { + fn from(error: Error) -> http::Response> { + http::Response::builder() + .header(http::header::CONTENT_TYPE, "application/json") + .status(error.status_code) + .body(to_json_vec(&ErrorBody::from(error)).unwrap()) + .unwrap() + } +} diff --git a/ruma-client-api/src/lib.rs b/ruma-client-api/src/lib.rs new file mode 100644 index 00000000..375c159b --- /dev/null +++ b/ruma-client-api/src/lib.rs @@ -0,0 +1,15 @@ +//! Crate ruma_client_api contains serializable types for the requests and responses for each +//! endpoint in the [Matrix](https://matrix.org/) client API specification. These types can be +//! shared by client and server code. + +#![deny( + missing_copy_implementations, + missing_debug_implementations, + missing_docs +)] + +pub mod error; +pub mod r0; +pub mod unversioned; + +pub use error::Error; diff --git a/ruma-client-api/src/r0.rs b/ruma-client-api/src/r0.rs new file mode 100644 index 00000000..23154ee7 --- /dev/null +++ b/ruma-client-api/src/r0.rs @@ -0,0 +1,35 @@ +//! Endpoints for the r0.x.x versions of the client API specification. + +pub mod account; +pub mod alias; +pub mod appservice; +pub mod capabilities; +pub mod config; +pub mod contact; +pub mod context; +pub mod device; +pub mod directory; +pub mod filter; +pub mod keys; +pub mod media; +pub mod membership; +pub mod message; +pub mod presence; +pub mod profile; +pub mod push; +pub mod read_marker; +pub mod receipt; +pub mod redact; +pub mod room; +pub mod search; +pub mod server; +pub mod session; +pub mod state; +pub mod sync; +pub mod tag; +pub mod thirdparty; +pub mod to_device; +pub mod typing; +pub mod uiaa; +pub mod user_directory; +pub mod voip; diff --git a/ruma-client-api/src/r0/account.rs b/ruma-client-api/src/r0/account.rs new file mode 100644 index 00000000..f1a70ac8 --- /dev/null +++ b/ruma-client-api/src/r0/account.rs @@ -0,0 +1,43 @@ +//! Endpoints for account registration and management. + +pub mod bind_3pid; +pub mod change_password; +pub mod deactivate; +pub mod delete_3pid; +pub mod get_username_availability; +pub mod register; +pub mod request_3pid_management_token_via_email; +pub mod request_3pid_management_token_via_msisdn; +pub mod request_openid_token; +pub mod request_password_change_token_via_email; +pub mod request_password_change_token_via_msisdn; +pub mod request_registration_token_via_email; +pub mod request_registration_token_via_msisdn; +pub mod unbind_3pid; + +pub mod whoami; + +use serde::{Deserialize, Serialize}; + +/// Additional authentication information for requestToken endpoints. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct IdentityServerInfo { + /// The ID server to send the onward request to as a hostname with an + /// appended colon and port number if the port is not the default. + pub id_server: String, + + /// Access token previously registered with identity server. + pub id_access_token: String, +} + +/// Possible values for deleting or unbinding 3PIDs +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ThirdPartyIdRemovalStatus { + /// Either the homeserver couldn't determine the right identity server to contact, or the + /// identity server refused the operation. + NoSupport, + + /// Success. + Success, +} diff --git a/ruma-client-api/src/r0/account/add_3pid.rs b/ruma-client-api/src/r0/account/add_3pid.rs new file mode 100644 index 00000000..44b663b2 --- /dev/null +++ b/ruma-client-api/src/r0/account/add_3pid.rs @@ -0,0 +1,32 @@ +//! [POST /_matrix/client/r0/account/3pid/add](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-add) + +use ruma_api::ruma_api; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +ruma_api! { + metadata { + description: "Add contact information to a user's account", + method: POST, + name: "add_3pid", + path: "/_matrix/client/r0/account/3pid/add", + rate_limited: true, + requires_authentication: true, + } + + request { + /// Additional information for the User-Interactive Authentication API. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// The session identifier given by the identity server. + pub sid: String, + } + + response {} + + error: UiaaResponse +} diff --git a/ruma-client-api/src/r0/account/bind_3pid.rs b/ruma-client-api/src/r0/account/bind_3pid.rs new file mode 100644 index 00000000..d13c31b2 --- /dev/null +++ b/ruma-client-api/src/r0/account/bind_3pid.rs @@ -0,0 +1,33 @@ +//! [POST /_matrix/client/r0/account/3pid/bind](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-bind) + +use ruma_api::ruma_api; + +use super::IdentityServerInfo; + +ruma_api! { + metadata { + description: "Bind a 3PID to a user's account on an identity server", + method: POST, + name: "bind_3pid", + path: "/_matrix/client/r0/account/3pid/bind", + rate_limited: true, + requires_authentication: true, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// The ID server to send the onward request to as a hostname with an + /// appended colon and port number if the port is not the default. + #[serde(flatten)] + pub identity_server_info: IdentityServerInfo, + + /// The session identifier given by the identity server. + pub sid: String, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/change_password.rs b/ruma-client-api/src/r0/account/change_password.rs new file mode 100644 index 00000000..8a4223d1 --- /dev/null +++ b/ruma-client-api/src/r0/account/change_password.rs @@ -0,0 +1,28 @@ +//! [POST /_matrix/client/r0/account/password](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-password) + +use ruma_api::ruma_api; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +ruma_api! { + metadata { + description: "Change the password of the current user's account.", + method: POST, + name: "change_password", + path: "/_matrix/client/r0/account/password", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The new password for the account. + pub new_password: String, + + /// Additional authentication information for the user-interactive authentication API. + pub auth: Option, + } + + response {} + + error: UiaaResponse +} diff --git a/ruma-client-api/src/r0/account/deactivate.rs b/ruma-client-api/src/r0/account/deactivate.rs new file mode 100644 index 00000000..a46b666a --- /dev/null +++ b/ruma-client-api/src/r0/account/deactivate.rs @@ -0,0 +1,36 @@ +//! [POST /_matrix/client/r0/account/deactivate](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-deactivate) + +use ruma_api::ruma_api; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +use super::ThirdPartyIdRemovalStatus; + +ruma_api! { + metadata { + description: "Deactivate the current user's account.", + method: POST, + name: "deactivate", + path: "/_matrix/client/r0/account/deactivate", + rate_limited: true, + requires_authentication: true, + } + + request { + /// Additional authentication information for the user-interactive authentication API. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + + /// Identity server from which to unbind the user's third party + /// identifier. + #[serde(skip_serializing_if = "Option::is_none")] + pub id_server: Option, + } + + response { + /// Result of unbind operation. + pub id_server_unbind_result: ThirdPartyIdRemovalStatus, + } + + error: UiaaResponse +} diff --git a/ruma-client-api/src/r0/account/delete_3pid.rs b/ruma-client-api/src/r0/account/delete_3pid.rs new file mode 100644 index 00000000..3138cdb5 --- /dev/null +++ b/ruma-client-api/src/r0/account/delete_3pid.rs @@ -0,0 +1,37 @@ +//! [POST /_matrix/client/r0/account/3pid/delete](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-delete) + +use ruma_api::ruma_api; + +use super::ThirdPartyIdRemovalStatus; +use crate::r0::thirdparty::Medium; + +ruma_api! { + metadata { + description: "Delete a 3PID from a user's account on an identity server.", + method: POST, + name: "delete_3pid", + path: "/_matrix/client/r0/account/3pid/delete", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Identity server to delete from. + #[serde(skip_serializing_if = "Option::is_none")] + pub id_server: Option, + + /// Medium of the 3PID to be removed. + pub medium: Medium, + + /// Third-party address being removed. + pub address: String, + } + + response { + /// Result of unbind operation. + pub id_server_unbind_result: ThirdPartyIdRemovalStatus, + } + + error: crate::Error + +} diff --git a/ruma-client-api/src/r0/account/get_username_availability.rs b/ruma-client-api/src/r0/account/get_username_availability.rs new file mode 100644 index 00000000..ffe1be74 --- /dev/null +++ b/ruma-client-api/src/r0/account/get_username_availability.rs @@ -0,0 +1,28 @@ +//! [GET /_matrix/client/r0/register/available](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Checks to see if a username is available, and valid, for the server.", + method: GET, + name: "get_username_availability", + path: "/_matrix/client/r0/register/available", + rate_limited: true, + requires_authentication: false, + } + + request { + /// The username to check the availability of. + #[ruma_api(query)] + pub username: String, + } + + response { + /// A flag to indicate that the username is available. + /// This should always be true when the server replies with 200 OK. + pub available: bool + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/register.rs b/ruma-client-api/src/r0/account/register.rs new file mode 100644 index 00000000..1c14e99a --- /dev/null +++ b/ruma-client-api/src/r0/account/register.rs @@ -0,0 +1,98 @@ +//! [POST /_matrix/client/r0/register](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-register) + +use ruma_api::ruma_api; +use ruma_identifiers::{DeviceId, UserId}; +use serde::{Deserialize, Serialize}; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +ruma_api! { + metadata { + description: "Register an account on this homeserver.", + method: POST, + name: "register", + path: "/_matrix/client/r0/register", + rate_limited: true, + requires_authentication: false, + } + + request { + /// The desired password for the account. + /// + /// May be empty for accounts that should not be able to log in again + /// with a password, e.g., for guest or application service accounts. + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + + /// local part of the desired Matrix ID. + /// + /// If omitted, the homeserver MUST generate a Matrix ID local part. + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + + /// ID of the client device. + /// + /// If this does not correspond to a known client device, a new device will be created. + /// The server will auto-generate a device_id if this is not specified. + #[serde(skip_serializing_if = "Option::is_none")] + pub device_id: Option, + + /// A display name to assign to the newly-created device. + /// + /// Ignored if `device_id` corresponds to a known device. + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_device_display_name: Option, + + /// Additional authentication information for the user-interactive authentication API. + /// + /// Note that this information is not used to define how the registered user should be + /// authenticated, but is instead used to authenticate the register call itself. + /// It should be left empty, or omitted, unless an earlier call returned an response + /// with status code 401. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + + /// Kind of account to register + /// + /// Defaults to `User` if omitted. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + + /// If `true`, an `access_token` and `device_id` should not be returned + /// from this call, therefore preventing an automatic login. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub inhibit_login: bool, + } + + response { + /// An access token for the account. + /// + /// This access token can then be used to authorize other requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub access_token: Option, + + /// The fully-qualified Matrix ID that has been registered. + pub user_id: UserId, + + /// ID of the registered device. + /// + /// Will be the same as the corresponding parameter in the request, if one was specified. + pub device_id: Option, + } + + error: UiaaResponse +} + +/// The kind of account being registered. +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RegistrationKind { + /// A guest account + /// + /// These accounts may have limited permissions and may not be supported by all servers. + Guest, + + /// A regular user account + User, +} diff --git a/ruma-client-api/src/r0/account/request_3pid_management_token_via_email.rs b/ruma-client-api/src/r0/account/request_3pid_management_token_via_email.rs new file mode 100644 index 00000000..3042225b --- /dev/null +++ b/ruma-client-api/src/r0/account/request_3pid_management_token_via_email.rs @@ -0,0 +1,48 @@ +//! [POST /_matrix/client/r0/account/3pid/email/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-email-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::IdentityServerInfo; + +ruma_api! { + metadata { + description: "Request a 3PID management token with a 3rd party email.", + method: POST, + name: "request_3pid_association_token_via_email", + path: "/_matrix/client/r0/account/3pid/email/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// The email address. + pub email: String, + + /// Used to distinguish protocol level retries from requests to re-send the email. + pub send_attempt: UInt, + + /// Return URL for identity server to redirect the client back to. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + + /// Optional identity server hostname and access token. Deprecated since r0.6.0. + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_server_info: Option, + } + + response { + /// The session identifier given by the identity server. + pub sid: String, + + /// URL to submit validation token to. If omitted, verification happens without client. + #[serde(skip_serializing_if = "Option::is_none")] + pub submit_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/request_3pid_management_token_via_msisdn.rs b/ruma-client-api/src/r0/account/request_3pid_management_token_via_msisdn.rs new file mode 100644 index 00000000..21ce58af --- /dev/null +++ b/ruma-client-api/src/r0/account/request_3pid_management_token_via_msisdn.rs @@ -0,0 +1,51 @@ +//! [POST /_matrix/client/r0/account/3pid/msisdn/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-msisdn-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::IdentityServerInfo; + +ruma_api! { + metadata { + description: "Request a 3PID management token with a phone number.", + method: POST, + name: "request_3pid_association_token_via_msisdn", + path: "/_matrix/client/r0/account/3pid/msisdn/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// Two-letter ISO 3166 country code for the phone number. + pub country: String, + + /// Phone number to validate. + pub phone_number: String, + + /// Used to distinguish protocol level retries from requests to re-send the SMS. + pub send_attempt: UInt, + + /// Return URL for identity server to redirect the client back to. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + + /// Optional identity server hostname and access token. Deprecated since r0.6.0. + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_server_info: Option, + } + + response { + /// The session identifier given by the identity server. + pub sid: String, + + /// URL to submit validation token to. If omitted, verification happens without client. + #[serde(skip_serializing_if = "Option::is_none")] + pub submit_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/request_openid_token.rs b/ruma-client-api/src/r0/account/request_openid_token.rs new file mode 100644 index 00000000..94107189 --- /dev/null +++ b/ruma-client-api/src/r0/account/request_openid_token.rs @@ -0,0 +1,48 @@ +//! [POST /_matrix/client/r0/user/{userId}/openid/request_token](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-user-userid-openid-request-token) + +use std::time::Duration; + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Request an OpenID 1.0 token to verify identity with a third party.", + name: "request_openid_token", + method: POST, + path: "/_matrix/client/r0/user/:user_id/openid/request_token", + rate_limited: true, + requires_authentication: true, + } + + request { + /// User ID of authenticated user. + #[ruma_api(path)] + pub user_id: UserId, + } + + response { + /// Access token for verifying user's identity. + pub access_token: String, + + /// Access token type. + pub token_type: TokenType, + + /// Homeserver domain for verification of user's identity. + pub matrix_server_name: String, + + /// Seconds until token expiration. + #[serde(with = "ruma_serde::duration::secs")] + pub expires_in: Duration, + } + + error: crate::Error +} + +/// Access token types. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum TokenType { + /// Bearer token type + Bearer, +} diff --git a/ruma-client-api/src/r0/account/request_password_change_token_via_email.rs b/ruma-client-api/src/r0/account/request_password_change_token_via_email.rs new file mode 100644 index 00000000..f796191f --- /dev/null +++ b/ruma-client-api/src/r0/account/request_password_change_token_via_email.rs @@ -0,0 +1,48 @@ +//! [POST /_matrix/client/r0/account/password/email/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-password-email-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::IdentityServerInfo; + +ruma_api! { + metadata { + description: "Request that a password change token is sent to the given email address.", + method: POST, + name: "request_password_change_token_via_email", + path: "/_matrix/client/r0/account/password/email/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// The email address. + pub email: String, + + /// Used to distinguish protocol level retries from requests to re-send the email. + pub send_attempt: UInt, + + /// Return URL for identity server to redirect the client back to. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + + /// Optional identity server hostname and access token. Deprecated since r0.6.0. + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_server_info: Option, + } + + response { + /// The session identifier given by the identity server. + pub sid: String, + + /// URL to submit validation token to. If omitted, verification happens without client. + #[serde(skip_serializing_if = "Option::is_none")] + pub submit_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/request_password_change_token_via_msisdn.rs b/ruma-client-api/src/r0/account/request_password_change_token_via_msisdn.rs new file mode 100644 index 00000000..7e14e2ad --- /dev/null +++ b/ruma-client-api/src/r0/account/request_password_change_token_via_msisdn.rs @@ -0,0 +1,44 @@ +//! [POST /_matrix/client/r0/account/password/msisdn/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-password-msisdn-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Request that a password change token is sent to the given phone number.", + method: POST, + name: "request_password_change_token_via_msisdn", + path: "/_matrix/client/r0/account/password/msisdn/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// Two-letter ISO 3166 country code for the phone number. + pub country: String, + + /// Phone number to validate. + pub phone_number: String, + + /// Used to distinguish protocol level retries from requests to re-send the SMS. + pub send_attempt: UInt, + + /// Return URL for identity server to redirect the client back to. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + } + + response { + /// The session identifier given by the identity server. + pub sid: String, + + /// URL to submit validation token to. If omitted, verification happens without client. + #[serde(skip_serializing_if = "Option::is_none")] + pub submit_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/request_registration_token_via_email.rs b/ruma-client-api/src/r0/account/request_registration_token_via_email.rs new file mode 100644 index 00000000..dc1c4cd0 --- /dev/null +++ b/ruma-client-api/src/r0/account/request_registration_token_via_email.rs @@ -0,0 +1,48 @@ +//! [POST /_matrix/client/r0/register/email/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-register-email-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::IdentityServerInfo; + +ruma_api! { + metadata { + description: "Request a registration token with a 3rd party email.", + method: POST, + name: "request_registration_token_via_email", + path: "/_matrix/client/r0/register/email/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// The email address. + pub email: String, + + /// Used to distinguish protocol level retries from requests to re-send the email. + pub send_attempt: UInt, + + /// Return URL for identity server to redirect the client back to. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + + /// Optional identity server hostname and access token. Deprecated since r0.6.0. + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_server_info: Option, + } + + response { + /// The session identifier given by the identity server. + pub sid: String, + + /// URL to submit validation token to. If omitted, verification happens without client. + #[serde(skip_serializing_if = "Option::is_none")] + pub submit_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/request_registration_token_via_msisdn.rs b/ruma-client-api/src/r0/account/request_registration_token_via_msisdn.rs new file mode 100644 index 00000000..afe7e53b --- /dev/null +++ b/ruma-client-api/src/r0/account/request_registration_token_via_msisdn.rs @@ -0,0 +1,51 @@ +//! [POST /_matrix/client/r0/register/msisdn/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-register-msisdn-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::IdentityServerInfo; + +ruma_api! { + metadata { + description: "Request a registration token with a phone number.", + method: POST, + name: "request_registration_token_via_msisdn", + path: "/_matrix/client/r0/register/msisdn/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// Two-letter ISO 3166 country code for the phone number. + pub country: String, + + /// Phone number to validate. + pub phone_number: String, + + /// Return URL for identity server to redirect the client back to. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + + /// Used to distinguish protocol level retries from requests to re-send the SMS. + pub send_attempt: UInt, + + /// Optional identity server hostname and access token. Deprecated since r0.6.0. + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_server_info: Option, + } + + response { + /// The session identifier given by the identity server. + pub sid: String, + + /// URL to submit validation token to. If omitted, verification happens without client. + #[serde(skip_serializing_if = "Option::is_none")] + pub submit_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/unbind_3pid.rs b/ruma-client-api/src/r0/account/unbind_3pid.rs new file mode 100644 index 00000000..8bdd7098 --- /dev/null +++ b/ruma-client-api/src/r0/account/unbind_3pid.rs @@ -0,0 +1,36 @@ +//! [POST /_matrix/client/r0/account/3pid/unbind](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-unbind) + +use ruma_api::ruma_api; + +use super::ThirdPartyIdRemovalStatus; +use crate::r0::thirdparty::Medium; + +ruma_api! { + metadata { + description: "Unbind a 3PID from a user's account on an identity server.", + method: POST, + name: "unbind_3pid", + path: "/_matrix/client/r0/account/3pid/unbind", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Identity server to unbind from. + #[serde(skip_serializing_if = "Option::is_none")] + pub id_server: Option, + + /// Medium of the 3PID to be removed. + pub medium: Medium, + + /// Third-party address being removed. + pub address: String, + } + + response { + /// Result of unbind operation. + pub id_server_unbind_result: ThirdPartyIdRemovalStatus, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/account/whoami.rs b/ruma-client-api/src/r0/account/whoami.rs new file mode 100644 index 00000000..cee6c52a --- /dev/null +++ b/ruma-client-api/src/r0/account/whoami.rs @@ -0,0 +1,23 @@ +//! [GET /_matrix/client/r0/account/whoami](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-account-whoami) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Get information about the owner of a given access token.", + method: GET, + name: "whoami", + path: "/_matrix/client/r0/account/whoami", + rate_limited: true, + requires_authentication: true, + } + + request {} + + response { + /// The id of the user that owns the access token. + pub user_id: String, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/alias.rs b/ruma-client-api/src/r0/alias.rs new file mode 100644 index 00000000..574c62c9 --- /dev/null +++ b/ruma-client-api/src/r0/alias.rs @@ -0,0 +1,5 @@ +//! Endpoints for room aliases. + +pub mod create_alias; +pub mod delete_alias; +pub mod get_alias; diff --git a/ruma-client-api/src/r0/alias/create_alias.rs b/ruma-client-api/src/r0/alias/create_alias.rs new file mode 100644 index 00000000..0c84c30c --- /dev/null +++ b/ruma-client-api/src/r0/alias/create_alias.rs @@ -0,0 +1,28 @@ +//! [PUT /_matrix/client/r0/directory/room/{roomAlias}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-directory-room-roomalias) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomAliasId, RoomId}; + +ruma_api! { + metadata { + description: "Add an alias to a room.", + method: PUT, + name: "create_alias", + path: "/_matrix/client/r0/directory/room/:room_alias", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room alias to set. + #[ruma_api(path)] + pub room_alias: RoomAliasId, + + /// The room ID to set. + pub room_id: RoomId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/alias/delete_alias.rs b/ruma-client-api/src/r0/alias/delete_alias.rs new file mode 100644 index 00000000..c8dbedc2 --- /dev/null +++ b/ruma-client-api/src/r0/alias/delete_alias.rs @@ -0,0 +1,25 @@ +//! [DELETE /_matrix/client/r0/directory/room/{roomAlias}](https://matrix.org/docs/spec/client_server/r0.6.0#delete-matrix-client-r0-directory-room-roomalias) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomAliasId; + +ruma_api! { + metadata { + description: "Remove an alias from a room.", + method: DELETE, + name: "delete_alias", + path: "/_matrix/client/r0/directory/room/:room_alias", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room alias to remove. + #[ruma_api(path)] + pub room_alias: RoomAliasId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/alias/get_alias.rs b/ruma-client-api/src/r0/alias/get_alias.rs new file mode 100644 index 00000000..8e462a7c --- /dev/null +++ b/ruma-client-api/src/r0/alias/get_alias.rs @@ -0,0 +1,31 @@ +//! [GET /_matrix/client/r0/directory/room/{roomAlias}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-directory-room-roomalias) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomAliasId, RoomId}; + +ruma_api! { + metadata { + description: "Resolve a room alias to a room ID.", + method: GET, + name: "get_alias", + path: "/_matrix/client/r0/directory/room/:room_alias", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room alias. + #[ruma_api(path)] + pub room_alias: RoomAliasId, + } + + response { + /// The room ID for this room alias. + pub room_id: RoomId, + + /// A list of servers that are aware of this room ID. + pub servers: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/appservice.rs b/ruma-client-api/src/r0/appservice.rs new file mode 100644 index 00000000..50dd6665 --- /dev/null +++ b/ruma-client-api/src/r0/appservice.rs @@ -0,0 +1,3 @@ +//! Endpoints part of the application service extension of the client-server API + +pub mod set_room_visibility; diff --git a/ruma-client-api/src/r0/appservice/set_room_visibility.rs b/ruma-client-api/src/r0/appservice/set_room_visibility.rs new file mode 100644 index 00000000..f819e762 --- /dev/null +++ b/ruma-client-api/src/r0/appservice/set_room_visibility.rs @@ -0,0 +1,34 @@ +//! [PUT /_matrix/client/r0/directory/list/appservice/{networkId}/{roomId}](https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-client-r0-directory-list-appservice-networkid-roomid) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +use crate::r0::room::Visibility; + +ruma_api! { + metadata { + description: "Updates the visibility of a given room on the application service's room directory.", + method: PUT, + name: "set_room_visibility", + path: "/_matrix/client/r0/directory/list/appservice/:network_id/:room_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The protocol (network) ID to update the room list for. + #[ruma_api(path)] + pub network_id: String, + + /// The room ID to add to the directory. + #[ruma_api(path)] + pub room_id: RoomId, + + /// Whether the room should be visible (public) in the directory or not (private). + pub visibility: Visibility, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/capabilities.rs b/ruma-client-api/src/r0/capabilities.rs new file mode 100644 index 00000000..a0774dd8 --- /dev/null +++ b/ruma-client-api/src/r0/capabilities.rs @@ -0,0 +1,3 @@ +//! Endpoints for querying the server's supported feature set + +pub mod get_capabilities; diff --git a/ruma-client-api/src/r0/capabilities/get_capabilities.rs b/ruma-client-api/src/r0/capabilities/get_capabilities.rs new file mode 100644 index 00000000..b4f1e97a --- /dev/null +++ b/ruma-client-api/src/r0/capabilities/get_capabilities.rs @@ -0,0 +1,73 @@ +//! [GET /_matrix/client/r0/capabilities](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-capabilities) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomVersionId; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::collections::BTreeMap; + +ruma_api! { + metadata { + description: "Gets information about the server's supported feature set and other relevant capabilities.", + method: GET, + name: "get_capabilities", + path: "/_matrix/client/r0/capabilities", + rate_limited: true, + requires_authentication: true + } + + request {} + + response { + /// The capabilities the server supports + pub capabilities: Capabilities, + } + + error: crate::Error +} + +/// Contains information about all the capabilities that the server supports. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Capabilities { + /// Capability to indicate if the user can change their password. + #[serde(rename = "m.change_password", skip_serializing_if = "Option::is_none")] + pub change_password: Option, + + /// The room versions the server supports. + #[serde(rename = "m.room_versions", skip_serializing_if = "Option::is_none")] + pub room_versions: Option, + + /// Any other custom capabilities that the server supports outside of the specification, + /// labeled using the Java package naming convention and stored as arbitrary JSON values. + #[serde(flatten)] + pub custom_capabilities: BTreeMap, +} + +/// Information about the m.change_password capability +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct ChangePasswordCapability { + /// True if the user can change their password, false otherwise. + pub enabled: bool, +} + +/// Information about the m.room_versions capability +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RoomVersionsCapability { + /// The default room version the server is using for new rooms. + pub default: String, + + /// A detailed description of the room versions the server supports. + pub available: BTreeMap, +} + +/// The stability of a room version +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum RoomVersionStability { + /// Support for the given version is stable. + #[serde(rename = "stable")] + Stable, + + /// Support for the given version is unstable. + #[serde(rename = "unstable")] + Unstable, +} diff --git a/ruma-client-api/src/r0/config.rs b/ruma-client-api/src/r0/config.rs new file mode 100644 index 00000000..425c22ef --- /dev/null +++ b/ruma-client-api/src/r0/config.rs @@ -0,0 +1,6 @@ +//! Endpoints for client configuration. + +pub mod get_global_account_data; +pub mod get_room_account_data; +pub mod set_global_account_data; +pub mod set_room_account_data; diff --git a/ruma-client-api/src/r0/config/get_global_account_data.rs b/ruma-client-api/src/r0/config/get_global_account_data.rs new file mode 100644 index 00000000..d8c1d763 --- /dev/null +++ b/ruma-client-api/src/r0/config/get_global_account_data.rs @@ -0,0 +1,34 @@ +//! [GET /_matrix/client/r0/user/{userId}/account_data/{type}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-user-userid-account-data-type) + +use ruma_api::ruma_api; +use ruma_events::{collections::only, EventJson}; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Gets global account data for a user.", + name: "get_global_account_data", + method: GET, + path: "/_matrix/client/r0/user/:user_id/account_data/:event_type", + rate_limited: false, + requires_authentication: true, + } + + request { + /// User ID of user for whom to retrieve data. + #[ruma_api(path)] + pub user_id: UserId, + + /// Type of data to retrieve. + #[ruma_api(path)] + pub event_type: String, + } + + response { + /// Account data content for the given type. + #[ruma_api(body)] + pub account_data: EventJson, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/config/get_room_account_data.rs b/ruma-client-api/src/r0/config/get_room_account_data.rs new file mode 100644 index 00000000..a2b39600 --- /dev/null +++ b/ruma-client-api/src/r0/config/get_room_account_data.rs @@ -0,0 +1,38 @@ +//! [GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-user-userid-rooms-roomid-account-data-type) + +use ruma_api::ruma_api; +use ruma_events::{collections::only, EventJson}; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Gets account data room for a user for a given room", + name: "get_room_account_data", + method: GET, + path: "/_matrix/client/r0/user/:user_id/rooms/:room_id/account_data/:event_type", + rate_limited: false, + requires_authentication: true, + } + + request { + /// User ID of user for whom to retrieve data. + #[ruma_api(path)] + pub user_id: UserId, + + /// Room ID for which to retrieve data. + #[ruma_api(path)] + pub room_id: RoomId, + + /// Type of data to retrieve. + #[ruma_api(path)] + pub event_type: String, + } + + response { + /// Account data content for the given type. + #[ruma_api(body)] + pub account_data: EventJson, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/config/set_global_account_data.rs b/ruma-client-api/src/r0/config/set_global_account_data.rs new file mode 100644 index 00000000..9214271c --- /dev/null +++ b/ruma-client-api/src/r0/config/set_global_account_data.rs @@ -0,0 +1,40 @@ +//! [PUT /_matrix/client/r0/user/{userId}/account_data/{type}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-user-userid-account-data-type) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Sets global account data.", + method: PUT, + name: "set_global_account_data", + path: "/_matrix/client/r0/user/:user_id/account_data/:event_type", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Arbitrary JSON to store as config data. + /// + /// To create a `Box`, use `serde_json::value::to_raw_value`. + #[ruma_api(body)] + pub data: Box, + + /// The event type of the account_data to set. + /// + /// Custom types should be namespaced to avoid clashes. + #[ruma_api(path)] + pub event_type: String, + + /// The ID of the user to set account_data for. + /// + /// The access token must be authorized to make requests for this user ID. + #[ruma_api(path)] + pub user_id: UserId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/config/set_room_account_data.rs b/ruma-client-api/src/r0/config/set_room_account_data.rs new file mode 100644 index 00000000..457ee643 --- /dev/null +++ b/ruma-client-api/src/r0/config/set_room_account_data.rs @@ -0,0 +1,44 @@ +//! [PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-user-userid-rooms-roomid-account-data-type) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Associate account data with a room.", + method: PUT, + name: "set_room_account_data", + path: "/_matrix/client/r0/user/:user_id/rooms/:room_id/account_data/:event_type", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Arbitrary JSON to store as config data. + /// + /// To create a `Box`, use `serde_json::value::to_raw_value`. + #[ruma_api(body)] + pub data: Box, + + /// The event type of the account_data to set. + /// + /// Custom types should be namespaced to avoid clashes. + #[ruma_api(path)] + pub event_type: String, + + /// The ID of the room to set account_data on. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The ID of the user to set account_data for. + /// + /// The access token must be authorized to make requests for this user ID. + #[ruma_api(path)] + pub user_id: UserId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/contact.rs b/ruma-client-api/src/r0/contact.rs new file mode 100644 index 00000000..41c13004 --- /dev/null +++ b/ruma-client-api/src/r0/contact.rs @@ -0,0 +1,4 @@ +//! Endpoints for account contact information. + +pub mod get_contacts; +pub mod request_contact_verification_token; diff --git a/ruma-client-api/src/r0/contact/get_contacts.rs b/ruma-client-api/src/r0/contact/get_contacts.rs new file mode 100644 index 00000000..06de18bc --- /dev/null +++ b/ruma-client-api/src/r0/contact/get_contacts.rs @@ -0,0 +1,84 @@ +//! [GET /_matrix/client/r0/account/3pid](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-account-3pid) + +use std::time::SystemTime; + +use ruma_api::ruma_api; +use serde::{Deserialize, Serialize}; + +use crate::r0::thirdparty::Medium; + +ruma_api! { + metadata { + description: "Get a list of 3rd party contacts associated with the user's account.", + method: GET, + name: "get_contacts", + path: "/_matrix/client/r0/account/3pid", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// A list of third party identifiers the homeserver has associated with the user's + /// account. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub threepids: Vec, + } + + error: crate::Error +} + +/// An identifier external to Matrix. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ThirdPartyIdentifier { + /// The third party identifier address. + pub address: String, + + /// The medium of third party identifier. + pub medium: Medium, + + /// The time when the identifier was validated by the identity server. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub validated_at: SystemTime, + + /// The time when the homeserver associated the third party identifier with the user. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub added_at: SystemTime, +} + +#[cfg(test)] +mod tests { + use std::time::{Duration, UNIX_EPOCH}; + + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::{Medium, ThirdPartyIdentifier}; + + #[test] + fn third_party_identifier_serde() { + let third_party_id = ThirdPartyIdentifier { + address: "monkey@banana.island".into(), + medium: Medium::Email, + validated_at: UNIX_EPOCH + Duration::from_millis(1_535_176_800_000), + added_at: UNIX_EPOCH + Duration::from_millis(1_535_336_848_756), + }; + + let third_party_id_serialized = json!({ + "medium": "email", + "address": "monkey@banana.island", + "validated_at": 1_535_176_800_000u64, + "added_at": 1_535_336_848_756u64 + }); + + assert_eq!( + to_json_value(third_party_id.clone()).unwrap(), + third_party_id_serialized + ); + assert_eq!( + third_party_id, + from_json_value(third_party_id_serialized).unwrap() + ); + } +} diff --git a/ruma-client-api/src/r0/contact/request_contact_verification_token.rs b/ruma-client-api/src/r0/contact/request_contact_verification_token.rs new file mode 100644 index 00000000..03cda2ee --- /dev/null +++ b/ruma-client-api/src/r0/contact/request_contact_verification_token.rs @@ -0,0 +1,47 @@ +//! [POST /_matrix/client/r0/account/3pid/email/requestToken](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-account-3pid-email-requesttoken) + +use js_int::UInt; +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Ask for a verification token for a given 3rd party ID.", + method: POST, + name: "request_contact_verification_token", + path: "/_matrix/client/r0/account/3pid/email/requestToken", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Client-generated secret string used to protect this session. + pub client_secret: String, + + /// The email address. + pub email: String, + + /// A URL for the identity server to redirect the user to after + /// validation is completed. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_link: Option, + + /// Used to distinguish protocol level retries from requests to re-send + /// the email. + pub send_attempt: UInt, + + /// The identity server to send the onward request to as a hostname with + /// an appended colon and port number if the port is not the default. + #[serde(skip_serializing_if = "Option::is_none")] + pub id_server: Option, + + /// An access token previously registered with the identity server. + /// + /// Required if an `id_server` is supplied. + #[serde(skip_serializing_if = "Option::is_none")] + pub id_access_token: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/context.rs b/ruma-client-api/src/r0/context.rs new file mode 100644 index 00000000..dc9b03e7 --- /dev/null +++ b/ruma-client-api/src/r0/context.rs @@ -0,0 +1,3 @@ +//! Endpoints for event context. + +pub mod get_context; diff --git a/ruma-client-api/src/r0/context/get_context.rs b/ruma-client-api/src/r0/context/get_context.rs new file mode 100644 index 00000000..14cdc610 --- /dev/null +++ b/ruma-client-api/src/r0/context/get_context.rs @@ -0,0 +1,84 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/context/{eventId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-context-eventid) + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{collections::all, EventJson}; +use ruma_identifiers::{EventId, RoomId}; + +use crate::r0::filter::RoomEventFilter; + +ruma_api! { + metadata { + description: "Get the events immediately preceding and following a given event.", + method: GET, + path: "/_matrix/client/r0/rooms/:room_id/context/:event_id", + name: "get_context", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to get events from. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The event to get context around. + #[ruma_api(path)] + pub event_id: EventId, + + /// The maximum number of events to return. + /// + /// Defaults to 10. + #[ruma_api(query)] + #[serde(default = "default_limit", skip_serializing_if = "is_default_limit")] + pub limit: UInt, + + /// A RoomEventFilter to filter returned events with. + #[ruma_api(query)] + #[serde( + with = "ruma_serde::json_string", + default, + skip_serializing_if = "Option::is_none" + )] + pub filter: Option, + } + + response { + /// A token that can be used to paginate backwards with. + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, + + /// A token that can be used to paginate forwards with. + #[serde(skip_serializing_if = "Option::is_none")] + pub end: Option, + + /// A list of room events that happened just before the requested event, + /// in reverse-chronological order. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events_before: Vec>, + + /// Details of the requested event. + #[serde(skip_serializing_if = "Option::is_none")] + pub event: Option>, + + /// A list of room events that happened just after the requested event, + /// in chronological order. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events_after: Vec>, + + /// The state of the room at the last event returned. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub state: Vec>, + } + + error: crate::Error +} + +fn default_limit() -> UInt { + UInt::from(10u32) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_default_limit(val: &UInt) -> bool { + *val == default_limit() +} diff --git a/ruma-client-api/src/r0/device.rs b/ruma-client-api/src/r0/device.rs new file mode 100644 index 00000000..685dab81 --- /dev/null +++ b/ruma-client-api/src/r0/device.rs @@ -0,0 +1,33 @@ +//! Endpoints for managing devices. + +use std::time::SystemTime; + +use ruma_identifiers::DeviceId; +use serde::{Deserialize, Serialize}; + +pub mod delete_device; +pub mod delete_devices; +pub mod get_device; +pub mod get_devices; +pub mod update_device; + +/// Information about a registered device. +#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)] +pub struct Device { + /// Device ID + pub device_id: DeviceId, + + /// Public display name of the device. + pub display_name: Option, + + /// Most recently seen IP address of the session. + pub last_seen_ip: Option, + + /// Unix timestamp that the session was last active. + #[serde( + with = "ruma_serde::time::opt_ms_since_unix_epoch", + default, + skip_serializing_if = "Option::is_none" + )] + pub last_seen_ts: Option, +} diff --git a/ruma-client-api/src/r0/device/delete_device.rs b/ruma-client-api/src/r0/device/delete_device.rs new file mode 100644 index 00000000..19a8df30 --- /dev/null +++ b/ruma-client-api/src/r0/device/delete_device.rs @@ -0,0 +1,31 @@ +//! [DELETE /_matrix/client/r0/devices/{deviceId}](https://matrix.org/docs/spec/client_server/r0.6.0#delete-matrix-client-r0-devices-deviceid) + +use ruma_api::ruma_api; +use ruma_identifiers::DeviceId; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +ruma_api! { + metadata { + description: "Delete a device for authenticated user.", + method: DELETE, + name: "delete_device", + path: "/_matrix/client/r0/devices/:device_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The device to delete. + #[ruma_api(path)] + pub device_id: DeviceId, + + /// Additional authentication information for the user-interactive authentication API. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + } + + response {} + + error: UiaaResponse +} diff --git a/ruma-client-api/src/r0/device/delete_devices.rs b/ruma-client-api/src/r0/device/delete_devices.rs new file mode 100644 index 00000000..d8f35b6d --- /dev/null +++ b/ruma-client-api/src/r0/device/delete_devices.rs @@ -0,0 +1,30 @@ +//! [POST /_matrix/client/r0/delete_devices](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-delete-devices) + +use ruma_api::ruma_api; +use ruma_identifiers::DeviceId; + +use crate::r0::uiaa::{AuthData, UiaaResponse}; + +ruma_api! { + metadata { + description: "Delete specified devices.", + method: POST, + path: "/_matrix/client/r0/delete_devices", + name: "delete_devices", + rate_limited: false, + requires_authentication: true, + } + + request { + /// List of devices to delete. + pub devices: Vec, + + /// Additional authentication information for the user-interactive authentication API. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + } + + response {} + + error: UiaaResponse +} diff --git a/ruma-client-api/src/r0/device/get_device.rs b/ruma-client-api/src/r0/device/get_device.rs new file mode 100644 index 00000000..7095162b --- /dev/null +++ b/ruma-client-api/src/r0/device/get_device.rs @@ -0,0 +1,30 @@ +//! [GET /_matrix/client/r0/devices/{deviceId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-devices-deviceid) + +use super::Device; +use ruma_api::ruma_api; +use ruma_identifiers::DeviceId; + +ruma_api! { + metadata { + description: "Get a device for authenticated user.", + method: GET, + name: "get_device", + path: "/_matrix/client/r0/devices/:device_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The device to retrieve. + #[ruma_api(path)] + pub device_id: DeviceId, + } + + response { + /// Information about the device. + #[ruma_api(body)] + pub device: Device, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/device/get_devices.rs b/ruma-client-api/src/r0/device/get_devices.rs new file mode 100644 index 00000000..0e6fef51 --- /dev/null +++ b/ruma-client-api/src/r0/device/get_devices.rs @@ -0,0 +1,24 @@ +//! [GET /_matrix/client/r0/devices](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-devices) + +use super::Device; +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Get registered devices for authenticated user.", + method: GET, + name: "get_devices", + path: "/_matrix/client/r0/devices", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// A list of all registered devices for this user + pub devices: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/device/update_device.rs b/ruma-client-api/src/r0/device/update_device.rs new file mode 100644 index 00000000..fdd30d67 --- /dev/null +++ b/ruma-client-api/src/r0/device/update_device.rs @@ -0,0 +1,30 @@ +//! [PUT /_matrix/client/r0/devices/{deviceId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-devices-deviceid) + +use ruma_api::ruma_api; +use ruma_identifiers::DeviceId; + +ruma_api! { + metadata { + description: "Update metadata for a device.", + method: PUT, + name: "update_device", + path: "/_matrix/client/r0/devices/:device_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The device to update. + #[ruma_api(path)] + pub device_id: DeviceId, + + /// The new display name for this device. If this is `None`, the display name won't be + /// changed. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/directory.rs b/ruma-client-api/src/r0/directory.rs new file mode 100644 index 00000000..e6ae3669 --- /dev/null +++ b/ruma-client-api/src/r0/directory.rs @@ -0,0 +1,48 @@ +//! Endpoints for the public room directory. + +pub mod get_public_rooms; +pub mod get_public_rooms_filtered; +pub mod get_room_visibility; +pub mod set_room_visibility; + +use js_int::UInt; +use ruma_identifiers::{RoomAliasId, RoomId}; +use serde::{Deserialize, Serialize}; + +/// 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, +} diff --git a/ruma-client-api/src/r0/directory/get_public_rooms.rs b/ruma-client-api/src/r0/directory/get_public_rooms.rs new file mode 100644 index 00000000..edac36b8 --- /dev/null +++ b/ruma-client-api/src/r0/directory/get_public_rooms.rs @@ -0,0 +1,52 @@ +//! [GET /_matrix/client/r0/publicRooms](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-publicrooms) + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::PublicRoomsChunk; + +ruma_api! { + metadata { + description: "Get the list of rooms in this homeserver's public directory.", + method: GET, + name: "get_public_rooms", + path: "/_matrix/client/r0/publicRooms", + rate_limited: false, + requires_authentication: false, + } + + request { + /// Limit for the number of results to return. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + + /// Pagination token from a previous request. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub since: Option, + + /// The server to fetch the public room lists from. + /// + /// `None` means the server this request is sent to. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub server: Option, + } + + response { + /// A paginated chunk of public rooms. + pub chunk: Vec, + + /// A pagination token for the response. + pub next_batch: Option, + + /// A pagination token that allows fetching previous results. + 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, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/directory/get_public_rooms_filtered.rs b/ruma-client-api/src/r0/directory/get_public_rooms_filtered.rs new file mode 100644 index 00000000..9680c55a --- /dev/null +++ b/ruma-client-api/src/r0/directory/get_public_rooms_filtered.rs @@ -0,0 +1,255 @@ +//! [POST /_matrix/client/r0/publicRooms](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-publicrooms) + +use std::fmt; + +use js_int::UInt; +use ruma_api::ruma_api; +use serde::{ + de::{MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use serde_json::Value as JsonValue; + +use super::PublicRoomsChunk; + +ruma_api! { + metadata { + description: "Get the list of rooms in this homeserver's public directory.", + method: POST, + name: "get_public_rooms_filtered", + path: "/_matrix/client/r0/publicRooms", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The server to fetch the public room lists from. + /// + /// `None` means the server this request is sent to. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub server: Option, + + /// Limit for the number of results to return. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// Pagination token from a previous request. + #[serde(skip_serializing_if = "Option::is_none")] + pub since: Option, + + /// Filter to apply to the results. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option, + + /// Network to fetch the public room lists from. + #[serde(flatten, skip_serializing_if = "ruma_serde::is_default")] + pub room_network: RoomNetwork, + } + + response { + /// A paginated chunk of public rooms. + pub chunk: Vec, + + /// A pagination token for the response. + pub next_batch: Option, + + /// A pagination token that allows fetching previous results. + 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, + } + + error: crate::Error +} + +/// A filter for public rooms lists +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Filter { + /// A string to search for in the room metadata, e.g. name, topic, canonical alias etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub generic_search_term: Option, +} + +/// 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, + }) + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::RoomNetwork; + + #[test] + fn test_serialize_matrix_network_only() { + let json = json!({}); + assert_eq!(to_json_value(RoomNetwork::Matrix).unwrap(), json); + } + + #[test] + fn test_deserialize_matrix_network_only() { + let json = json!({ "include_all_networks": false }); + assert_eq!( + from_json_value::(json).unwrap(), + RoomNetwork::Matrix + ); + } + + #[test] + fn test_serialize_default_network_is_empty() { + let json = json!({}); + assert_eq!(to_json_value(RoomNetwork::default()).unwrap(), json); + } + + #[test] + fn test_deserialize_empty_network_is_default() { + let json = json!({}); + assert_eq!( + from_json_value::(json).unwrap(), + RoomNetwork::default() + ); + } + + #[test] + fn test_serialize_include_all_networks() { + let json = json!({ "include_all_networks": true }); + assert_eq!(to_json_value(RoomNetwork::All).unwrap(), json); + } + + #[test] + fn test_deserialize_include_all_networks() { + let json = json!({ "include_all_networks": true }); + assert_eq!( + from_json_value::(json).unwrap(), + RoomNetwork::All + ); + } + + #[test] + fn test_serialize_third_party_network() { + let json = json!({ "third_party_instance_id": "freenode" }); + assert_eq!( + to_json_value(RoomNetwork::ThirdParty("freenode".to_string())).unwrap(), + json + ); + } + + #[test] + fn test_deserialize_third_party_network() { + let json = json!({ "third_party_instance_id": "freenode" }); + assert_eq!( + from_json_value::(json).unwrap(), + RoomNetwork::ThirdParty("freenode".to_string()) + ); + } + + #[test] + fn test_deserialize_include_all_networks_and_third_party_exclusivity() { + let json = json!({ "include_all_networks": true, "third_party_instance_id": "freenode" }); + assert_eq!( + from_json_value::(json) + .unwrap_err() + .to_string() + .as_str(), + "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive." + ); + } +} diff --git a/ruma-client-api/src/r0/directory/get_room_visibility.rs b/ruma-client-api/src/r0/directory/get_room_visibility.rs new file mode 100644 index 00000000..5ab16326 --- /dev/null +++ b/ruma-client-api/src/r0/directory/get_room_visibility.rs @@ -0,0 +1,30 @@ +//! [GET /_matrix/client/r0/directory/list/room/{roomId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-directory-list-room-roomid) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +use crate::r0::room::Visibility; + +ruma_api! { + metadata { + description: "Get the visibility of a public room on a directory.", + name: "get_room_visibility", + method: GET, + path: "/_matrix/client/r0/directory/list/room/:room_id", + rate_limited: false, + requires_authentication: false, + } + + request { + /// The ID of the room of which to request the visibility. + #[ruma_api(path)] + pub room_id: RoomId, + } + + response { + /// Visibility of the room. + pub visibility: Visibility, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/directory/set_room_visibility.rs b/ruma-client-api/src/r0/directory/set_room_visibility.rs new file mode 100644 index 00000000..dc04c5cd --- /dev/null +++ b/ruma-client-api/src/r0/directory/set_room_visibility.rs @@ -0,0 +1,30 @@ +//! [PUT /_matrix/client/r0/directory/list/room/{roomId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-directory-list-room-roomid) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +use crate::r0::room::Visibility; + +ruma_api! { + metadata { + description: "Set the visibility of a public room on a directory.", + name: "set_room_visibility", + method: PUT, + path: "/_matrix/client/r0/directory/list/room/:room_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The ID of the room of which to set the visibility. + #[ruma_api(path)] + pub room_id: RoomId, + + /// New visibility setting for the room. + pub visibility: Visibility, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/filter.rs b/ruma-client-api/src/r0/filter.rs new file mode 100644 index 00000000..ae004c32 --- /dev/null +++ b/ruma-client-api/src/r0/filter.rs @@ -0,0 +1,381 @@ +//! Endpoints for event filters. + +pub mod create_filter; +pub mod get_filter; + +use std::fmt; + +use js_int::UInt; +use ruma_identifiers::{RoomId, UserId}; +use serde::{ + de::{MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, +}; + +/// Format to use for returned events +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum EventFormat { + /// Client format, as described in the Client API. + Client, + + /// Raw events from federation. + Federation, +} + +/// Filters to be applied to room events +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct RoomEventFilter { + /// A list of event types to exclude. + /// + /// If this list is absent then no event types are excluded. A matching type will be excluded + /// even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any + /// sequence of characters. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub not_types: Vec, + + /// A list of room IDs to exclude. + /// + /// If this list is absent then no rooms are excluded. A matching room will be excluded even if + /// it is listed in the 'rooms' filter. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub not_rooms: Vec, + + /// The maximum number of events to return. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// A list of room IDs to include. + /// + /// If this list is absent then all rooms are included. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + + /// A list of sender IDs to exclude. + /// + /// If this list is absent then no senders are excluded. A matching sender will be excluded even + /// if it is listed in the 'senders' filter. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub not_senders: Vec, + + /// A list of senders IDs to include. + /// + /// If this list is absent then all senders are included. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub senders: Option>, + + /// A list of event types to include. + /// + /// If this list is absent then all event types are included. A '*' can be used as a wildcard to + /// match any sequence of characters. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub types: Option>, + + /// If `true` include only events with a URL key in their content. + /// If `false`, exclude such events. + /// + /// If this item is absent then all event types are included. + #[serde(skip_serializing_if = "Option::is_none")] + pub contains_url: Option, + + /// Options to control lazy-loading of membership events. + #[serde(flatten)] + pub lazy_load_options: LazyLoadOptions, +} + +impl RoomEventFilter { + /// A filter that can be used to ignore all room events + pub fn ignore_all() -> Self { + Self { + types: Some(vec![]), + ..Default::default() + } + } +} + +/// Filters to be applied to room data +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct RoomFilter { + /// Include rooms that the user has left in the sync. + /// + /// Defaults to false if not included. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_leave: Option, + + /// The per user account data to include for rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_data: Option, + + /// The message and state update events to include for rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline: Option, + + /// The events that aren't recorded in the room history, e.g. typing and receipts, to include + /// for rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub ephemeral: Option, + + /// The state events to include for rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + + /// A list of room IDs to exclude. + /// + /// If this list is absent then no rooms are excluded. A matching room will be excluded even if + /// it is listed in the 'rooms' filter. This filter is applied before the filters in + /// `ephemeral`, `state`, `timeline` or `account_data`. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub not_rooms: Vec, + + /// A list of room IDs to include. + /// + /// If this list is absent then all rooms are included. This filter is applied before the + /// filters in `ephemeral`, `state`, `timeline` or `account_data`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl RoomFilter { + /// A filter that can be used to ignore all room events (of any type) + pub fn ignore_all() -> Self { + Self { + rooms: Some(vec![]), + ..Default::default() + } + } +} + +/// Filter for not-room data +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Filter { + /// A list of event types to exclude. + /// + /// If this list is absent then no event types are excluded. A matching type will be excluded + /// even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any + /// sequence of characters. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub not_types: Vec, + + /// The maximum number of events to return. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// A list of senders IDs to include. + /// + /// If this list is absent then all senders are included. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub senders: Option>, + + /// A list of event types to include. + /// + /// If this list is absent then all event types are included. A '*' can be used as a wildcard to + /// match any sequence of characters. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub types: Option>, + + /// A list of sender IDs to exclude. + /// + /// If this list is absent then no senders are excluded. A matching sender will be excluded even + /// if it is listed in the 'senders' filter. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub not_senders: Vec, +} + +impl Filter { + /// A filter that can be used to ignore all events + pub fn ignore_all() -> Self { + Self { + types: Some(vec![]), + ..Default::default() + } + } +} + +/// A filter definition +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct FilterDefinition { + /// List of event fields to include. + /// + /// If this list is absent then all fields are included. The entries may include '.' charaters + /// to indicate sub-fields. So ['content.body'] will include the 'body' field of the 'content' + /// object. A literal '.' character in a field name may be escaped using a '\'. A server may + /// include more fields than were requested. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub event_fields: Option>, + + /// The format to use for events. + /// + /// 'client' will return the events in a format suitable for clients. 'federation' will return + /// the raw event as received over federation. The default is 'client'. + #[serde(skip_serializing_if = "Option::is_none")] + pub event_format: Option, + + /// The presence updates to include. + #[serde(skip_serializing_if = "Option::is_none")] + pub presence: Option, + + /// The user account data that isn't associated with rooms to include. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_data: Option, + + /// Filters to be applied to room data. + #[serde(skip_serializing_if = "Option::is_none")] + pub room: Option, +} + +impl FilterDefinition { + /// A filter that can be used to ignore all events + pub fn ignore_all() -> Self { + Self { + account_data: Some(Filter::ignore_all()), + room: Some(RoomFilter::ignore_all()), + presence: Some(Filter::ignore_all()), + ..Default::default() + } + } +} + +/// Specifies options for [lazy-loading membership events][lazy-loading] on +/// supported endpoints +/// +/// [lazy-loading]: https://matrix.org/docs/spec/client_server/r0.6.0#lazy-loading-room-members +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LazyLoadOptions { + /// Disables lazy-loading of membership events. + Disabled, + + /// Enables lazy-loading of events. + Enabled { + /// If `true`, sends all membership events for all events, even if they have + /// already been sent to the client. Defaults to `false`. + include_redundant_members: bool, + }, +} + +impl Serialize for LazyLoadOptions { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state; + match *self { + Self::Enabled { + include_redundant_members: true, + } => { + state = serializer.serialize_struct("LazyLoad", 2)?; + state.serialize_field("lazy_load_members", &true)?; + state.serialize_field("include_redundant_members", &true)?; + } + Self::Enabled { .. } => { + state = serializer.serialize_struct("LazyLoad", 1)?; + state.serialize_field("lazy_load_members", &true)?; + } + _ => { + state = serializer.serialize_struct("LazyLoad", 0)?; + } + } + state.end() + } +} + +impl Default for LazyLoadOptions { + fn default() -> Self { + Self::Disabled + } +} + +struct LazyLoadOptionsVisitor; + +impl<'de> Visitor<'de> for LazyLoadOptionsVisitor { + type Value = LazyLoadOptions; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Lazy load options") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut lazy_load_members = false; + let mut include_redundant_members = false; + while let Some((key, value)) = access.next_entry::()? { + match &*key { + "lazy_load_members" => lazy_load_members = value, + "include_redundant_members" => include_redundant_members = value, + _ => {} + }; + } + + Ok(if lazy_load_members { + LazyLoadOptions::Enabled { + include_redundant_members, + } + } else { + LazyLoadOptions::Disabled + }) + } +} + +impl<'de> Deserialize<'de> for LazyLoadOptions { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(LazyLoadOptionsVisitor) + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::LazyLoadOptions; + + #[test] + fn test_serializing_disabled_lazy_load() { + let lazy_load_options = LazyLoadOptions::Disabled; + assert_eq!(to_json_value(lazy_load_options).unwrap(), json!({})); + } + + #[test] + fn test_serializing_lazy_load_no_redundant() { + let lazy_load_options = LazyLoadOptions::Enabled { + include_redundant_members: false, + }; + assert_eq!( + to_json_value(lazy_load_options).unwrap(), + json!({ "lazy_load_members": true }) + ); + } + + #[test] + fn test_serializing_lazy_load_with_redundant() { + let lazy_load_options = LazyLoadOptions::Enabled { + include_redundant_members: true, + }; + assert_eq!( + to_json_value(lazy_load_options).unwrap(), + json!({ "lazy_load_members": true, "include_redundant_members": true }) + ); + } + + #[test] + fn test_deserializing_no_lazy_load() { + let json = json!({}); + assert_eq!( + from_json_value::(json).unwrap(), + LazyLoadOptions::Disabled, + ); + } + + #[test] + fn test_deserializing_ignore_redundant_members_when_no_lazy_load() { + let json = json!({ "include_redundant_members": true }); + assert_eq!( + from_json_value::(json).unwrap(), + LazyLoadOptions::Disabled, + ); + } +} diff --git a/ruma-client-api/src/r0/filter/create_filter.rs b/ruma-client-api/src/r0/filter/create_filter.rs new file mode 100644 index 00000000..a3d2dc1f --- /dev/null +++ b/ruma-client-api/src/r0/filter/create_filter.rs @@ -0,0 +1,36 @@ +//! [POST /_matrix/client/r0/user/{userId}/filter](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-user-userid-filter) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +use super::FilterDefinition; + +ruma_api! { + metadata { + description: "Create a new filter for event retrieval.", + method: POST, + name: "create_filter", + path: "/_matrix/client/r0/user/:user_id/filter", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The ID of the user uploading the filter. + /// + /// The access token must be authorized to make requests for this user ID. + #[ruma_api(path)] + pub user_id: UserId, + + /// The filter definition. + #[ruma_api(body)] + pub filter: FilterDefinition, + } + + response { + /// The ID of the filter that was created. + pub filter_id: String, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/filter/get_filter.rs b/ruma-client-api/src/r0/filter/get_filter.rs new file mode 100644 index 00000000..c69c57d9 --- /dev/null +++ b/ruma-client-api/src/r0/filter/get_filter.rs @@ -0,0 +1,35 @@ +//! [GET /_matrix/client/r0/user/{userId}/filter/{filterId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-user-userid-filter-filterid) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +use super::FilterDefinition; + +ruma_api! { + metadata { + description: "Retrieve a previously created filter.", + method: GET, + name: "get_filter", + path: "/_matrix/client/r0/user/:user_id/filter/:filter_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The user ID to download a filter for. + #[ruma_api(path)] + pub user_id: UserId, + + /// The ID of the filter to download. + #[ruma_api(path)] + pub filter_id: String, + } + + response { + /// The filter definition. + #[ruma_api(body)] + pub filter: FilterDefinition, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/keys.rs b/ruma-client-api/src/r0/keys.rs new file mode 100644 index 00000000..21a40267 --- /dev/null +++ b/ruma-client-api/src/r0/keys.rs @@ -0,0 +1,164 @@ +//! Endpoints for key management + +use std::{ + collections::BTreeMap, + convert::TryFrom, + fmt::{self, Debug, Display, Formatter}, +}; + +use ruma_events::Algorithm; +use ruma_identifiers::{DeviceId, UserId}; +use serde::{ + de::{self, Unexpected}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +pub mod claim_keys; +pub mod get_key_changes; +pub mod get_keys; +pub mod upload_keys; + +/// The basic key algorithms in the specification +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum KeyAlgorithm { + /// The Ed25519 signature algorithm. + #[serde(rename = "ed25519")] + Ed25519, + + /// The Curve25519 ECDH algorithm. + #[serde(rename = "curve25519")] + Curve25519, + + /// The Curve25519 ECDH algorithm, but the key also contains signatures + #[serde(rename = "signed_curve25519")] + SignedCurve25519, +} + +impl Display for KeyAlgorithm { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let algorithm_str = match *self { + KeyAlgorithm::Ed25519 => "ed25519", + KeyAlgorithm::Curve25519 => "curve25519", + KeyAlgorithm::SignedCurve25519 => "signed_curve25519", + }; + write!(f, "{}", algorithm_str)?; + Ok(()) + } +} + +impl TryFrom<&'_ str> for KeyAlgorithm { + type Error = &'static str; + fn try_from(s: &str) -> Result { + match s { + "ed25519" => Ok(KeyAlgorithm::Ed25519), + "curve25519" => Ok(KeyAlgorithm::Curve25519), + "signed_curve25519" => Ok(KeyAlgorithm::SignedCurve25519), + _ => Err("Unknown algorithm"), + } + } +} + +/// A key algorithm and a device id, combined with a ':' +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct AlgorithmAndDeviceId(pub KeyAlgorithm, pub DeviceId); + +impl Display for AlgorithmAndDeviceId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + +impl Serialize for AlgorithmAndDeviceId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for AlgorithmAndDeviceId { + #[allow(clippy::comparison_chain)] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + let parts = value.split(':').collect::>(); + + const EXPECTED: &str = "a string composed of an algorithm and a device id separated by ':'"; + + if parts.len() < 2 { + return Err(de::Error::invalid_type( + Unexpected::Other("string without a ':' separator"), + &EXPECTED, + )); + } else if parts.len() > 2 { + return Err(de::Error::invalid_type( + Unexpected::Other("string with more than one ':' separator"), + &EXPECTED, + )); + } + + let algorithm_result = KeyAlgorithm::try_from(parts[0]); + match algorithm_result { + Ok(algorithm) => Ok(AlgorithmAndDeviceId(algorithm, parts[1].to_string())), + Err(_) => Err(de::Error::invalid_value( + Unexpected::Str(parts[0]), + &"valid key algorithm", + )), + } + } +} + +/// Identity keys for a device. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeviceKeys { + /// The ID of the user the device belongs to. Must match the user ID used when logging in. + pub user_id: UserId, + + /// The ID of the device these keys belong to. Must match the device ID used when logging in. + pub device_id: DeviceId, + + /// The encryption algorithms supported by this device. + pub algorithms: Vec, + + /// Public identity keys. + pub keys: BTreeMap, + + /// Signatures for the device key object. + pub signatures: BTreeMap>, + + /// Additional data added to the device key information by intermediate servers, and + /// not covered by the signatures. + #[serde(skip_serializing_if = "Option::is_none")] + pub unsigned: Option, +} + +/// Additional data added to device key information by intermediate servers. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnsignedDeviceInfo { + /// The display name which the user set on the device. + pub device_display_name: Option, +} + +/// A key for the SignedCurve25519 algorithm +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedKey { + /// Base64-encoded 32-byte Curve25519 public key. + pub key: String, + + /// Signatures for the key object. + pub signatures: BTreeMap>, +} + +/// A one-time public key for "pre-key" messages. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OneTimeKey { + /// A key containing signatures, for the SignedCurve25519 algorithm. + SignedKey(SignedKey), + + /// A string-valued key, for the Ed25519 and Curve25519 algorithms. + Key(String), +} diff --git a/ruma-client-api/src/r0/keys/claim_keys.rs b/ruma-client-api/src/r0/keys/claim_keys.rs new file mode 100644 index 00000000..d894d400 --- /dev/null +++ b/ruma-client-api/src/r0/keys/claim_keys.rs @@ -0,0 +1,47 @@ +//! [POST /_matrix/client/r0/keys/claim](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-keys-claim) + +use std::collections::BTreeMap; + +use std::time::Duration; + +use ruma_api::ruma_api; +use ruma_identifiers::{DeviceId, UserId}; +use serde_json::Value as JsonValue; + +use super::{AlgorithmAndDeviceId, KeyAlgorithm, OneTimeKey}; + +ruma_api! { + metadata { + description: "Claims one-time keys for use in pre-key messages.", + method: POST, + name: "claim_keys", + path: "/_matrix/client/r0/keys/claim", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The time (in milliseconds) to wait when downloading keys from remote servers. + /// 10 seconds is the recommended default. + #[serde( + with = "ruma_serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + pub timeout: Option, + + /// The keys to be claimed. + pub one_time_keys: BTreeMap>, + } + + response { + /// If any remote homeservers could not be reached, they are recorded here. + /// The names of the properties are the names of the unreachable servers. + pub failures: BTreeMap, + + /// One-time keys for the queried devices. + pub one_time_keys: BTreeMap>>, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/keys/get_key_changes.rs b/ruma-client-api/src/r0/keys/get_key_changes.rs new file mode 100644 index 00000000..fb979f8d --- /dev/null +++ b/ruma-client-api/src/r0/keys/get_key_changes.rs @@ -0,0 +1,38 @@ +//! [GET /_matrix/client/r0/keys/changes](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-keys-changes) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Gets a list of users who have updated their device identity keys since a previous sync token.", + method: GET, + name: "get_key_changes", + path: "/_matrix/client/r0/keys/changes", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The desired start point of the list. + /// Should be the next_batch field from a response to an earlier call to /sync. + #[ruma_api(query)] + pub from: String, + + /// The desired end point of the list. + /// Should be the next_batch field from a recent call to /sync - typically the most recent such call. + #[ruma_api(query)] + pub to: String, + } + + response { + /// The Matrix User IDs of all users who updated their device identity keys. + pub changed: Vec, + + /// The Matrix User IDs of all users who may have left all the end-to-end + /// encrypted rooms they previously shared with the user. + pub left: Vec + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/keys/get_keys.rs b/ruma-client-api/src/r0/keys/get_keys.rs new file mode 100644 index 00000000..a8ec4dd2 --- /dev/null +++ b/ruma-client-api/src/r0/keys/get_keys.rs @@ -0,0 +1,55 @@ +//! [POST /_matrix/client/r0/keys/query](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-keys-query) + +use std::{collections::BTreeMap, time::Duration}; + +use ruma_api::ruma_api; +use ruma_identifiers::{DeviceId, UserId}; +use serde_json::Value as JsonValue; + +use super::DeviceKeys; + +ruma_api! { + metadata { + description: "Returns the current devices and identity keys for the given users.", + method: POST, + name: "get_keys", + path: "/_matrix/client/r0/keys/query", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The time (in milliseconds) to wait when downloading keys from remote + /// servers. 10 seconds is the recommended default. + #[serde( + with = "ruma_serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + pub timeout: Option, + + /// The keys to be downloaded. An empty list indicates all devices for + /// the corresponding user. + pub device_keys: BTreeMap>, + + /// If the client is fetching keys as a result of a device update + /// received in a sync request, this should be the 'since' token of that + /// sync request, or any later sync token. This allows the server to + /// ensure its response contains the keys advertised by the notification + /// in that sync. + #[serde(skip_serializing_if = "Option::is_none")] + pub token: Option, + } + + response { + /// If any remote homeservers could not be reached, they are recorded + /// here. The names of the properties are the names of the unreachable + /// servers. + pub failures: BTreeMap, + + /// Information on the queried devices. + pub device_keys: BTreeMap>, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/keys/upload_keys.rs b/ruma-client-api/src/r0/keys/upload_keys.rs new file mode 100644 index 00000000..95224f3c --- /dev/null +++ b/ruma-client-api/src/r0/keys/upload_keys.rs @@ -0,0 +1,37 @@ +//! [POST /_matrix/client/r0/keys/upload](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-keys-upload) + +use std::collections::BTreeMap; + +use js_int::UInt; +use ruma_api::ruma_api; + +use super::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey}; + +ruma_api! { + metadata { + description: "Publishes end-to-end encryption keys for the device.", + method: POST, + name: "upload_keys", + path: "/_matrix/client/r0/keys/upload", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Identity keys for the device. May be absent if no new identity keys are required. + #[serde(skip_serializing_if = "Option::is_none")] + pub device_keys: Option, + + /// One-time public keys for "pre-key" messages. + #[serde(skip_serializing_if = "Option::is_none")] + pub one_time_keys: Option>, + } + + response { + /// For each key algorithm, the number of unclaimed one-time keys of that + /// type currently held on the server for this device. + pub one_time_key_counts: BTreeMap + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/media.rs b/ruma-client-api/src/r0/media.rs new file mode 100644 index 00000000..91064a52 --- /dev/null +++ b/ruma-client-api/src/r0/media.rs @@ -0,0 +1,8 @@ +//! Endpoints for the media repository. + +pub mod create_content; +pub mod get_content; +pub mod get_content_as_filename; +pub mod get_content_thumbnail; +pub mod get_media_config; +pub mod get_media_preview; diff --git a/ruma-client-api/src/r0/media/create_content.rs b/ruma-client-api/src/r0/media/create_content.rs new file mode 100644 index 00000000..1f75f0de --- /dev/null +++ b/ruma-client-api/src/r0/media/create_content.rs @@ -0,0 +1,37 @@ +//! [POST /_matrix/media/r0/upload](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-media-r0-upload) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Upload content to the media store.", + method: POST, + name: "create_media_content", + path: "/_matrix/media/r0/upload", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The name of the file being uploaded. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + + /// The content type of the file being uploaded. + // TODO: This should be optional. + #[ruma_api(header = CONTENT_TYPE)] + pub content_type: String, + + /// The file contents to upload. + #[ruma_api(raw_body)] + pub file: Vec, + } + + response { + /// The MXC URI for the uploaded content. + pub content_uri: String, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/media/get_content.rs b/ruma-client-api/src/r0/media/get_content.rs new file mode 100644 index 00000000..94d625a4 --- /dev/null +++ b/ruma-client-api/src/r0/media/get_content.rs @@ -0,0 +1,45 @@ +//! [GET /_matrix/media/r0/download/{serverName}/{mediaId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-media-r0-download-servername-mediaid) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Retrieve content from the media store.", + method: GET, + name: "get_media_content", + path: "/_matrix/media/r0/download/:server_name/:media_id", + rate_limited: false, + requires_authentication: false, + } + + request { + /// The media ID from the mxc:// URI (the path component). + #[ruma_api(path)] + pub media_id: String, + + /// The server name from the mxc:// URI (the authoritory component). + #[ruma_api(path)] + pub server_name: String, + + /// Whether to fetch media deemed remote. + /// Used to prevent routing loops. Defaults to `true`. + #[ruma_api(query)] + pub allow_remote: Option, + } + + response { + /// The content that was previously uploaded. + #[ruma_api(raw_body)] + pub file: Vec, + + /// The content type of the file that was previously uploaded. + #[ruma_api(header = CONTENT_TYPE)] + pub content_type: String, + + /// The name of the file that was previously uploaded, if set. + #[ruma_api(header = CONTENT_DISPOSITION)] + pub content_disposition: String, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/media/get_content_as_filename.rs b/ruma-client-api/src/r0/media/get_content_as_filename.rs new file mode 100644 index 00000000..0664b331 --- /dev/null +++ b/ruma-client-api/src/r0/media/get_content_as_filename.rs @@ -0,0 +1,49 @@ +//! [GET /_matrix/media/r0/download/{serverName}/{mediaId}/{fileName}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-media-r0-download-servername-mediaid-filename) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Retrieve content from the media store, specifying a filename to return.", + method: GET, + name: "get_media_content_as_filename", + path: "/_matrix/media/r0/download/:server_name/:media_id/:filename", + rate_limited: false, + requires_authentication: false, + } + + request { + /// The media ID from the mxc:// URI (the path component). + #[ruma_api(path)] + pub media_id: String, + + /// The server name from the mxc:// URI (the authoritory component). + #[ruma_api(path)] + pub server_name: String, + + /// The filename to return in the `Content-Disposition` header. + #[ruma_api(path)] + pub filename: String, + + /// Whether to fetch media deemed remote. + /// Used to prevent routing loops. Defaults to `true`. + #[ruma_api(query)] + pub allow_remote: Option, + } + + response { + /// The content that was previously uploaded. + #[ruma_api(raw_body)] + pub file: Vec, + + /// The content type of the file that was previously uploaded. + #[ruma_api(header = CONTENT_TYPE)] + pub content_type: String, + + /// The name of the file that was previously uploaded, if set. + #[ruma_api(header = CONTENT_DISPOSITION)] + pub content_disposition: String, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/media/get_content_thumbnail.rs b/ruma-client-api/src/r0/media/get_content_thumbnail.rs new file mode 100644 index 00000000..ad73e327 --- /dev/null +++ b/ruma-client-api/src/r0/media/get_content_thumbnail.rs @@ -0,0 +1,71 @@ +//! [GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-media-r0-thumbnail-servername-mediaid) + +use js_int::UInt; +use ruma_api::ruma_api; +use serde::{Deserialize, Serialize}; + +/// The desired resizing method. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Method { + /// Crop the original to produce the requested image dimensions. + Crop, + + /// Maintain the original aspect ratio of the source image. + Scale, +} + +ruma_api! { + metadata { + description: "Get a thumbnail of content from the media store.", + method: GET, + name: "get_content_thumbnail", + path: "/_matrix/media/r0/thumbnail/:server_name/:media_id", + rate_limited: true, + requires_authentication: false, + } + + request { + /// Whether to fetch media deemed remote. + /// + /// Used to prevent routing loops. Defaults to `true`. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_remote: Option, + + /// The media ID from the mxc:// URI (the path component). + #[ruma_api(path)] + pub media_id: String, + + /// The server name from the mxc:// URI (the authoritory component). + #[ruma_api(path)] + pub server_name: String, + + /// The *desired* height of the thumbnail. The actual thumbnail may not match the size + /// specified. + #[ruma_api(query)] + pub height: UInt, + + /// The desired resizing method. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option, + + /// The *desired* width of the thumbnail. The actual thumbnail may not match the size + /// specified. + #[ruma_api(query)] + pub width: UInt, + } + + response { + /// The content type of the thumbnail. + #[ruma_api(header = CONTENT_TYPE)] + pub content_type: String, + + /// A thumbnail of the requested content. + #[ruma_api(raw_body)] + pub file: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/media/get_media_config.rs b/ruma-client-api/src/r0/media/get_media_config.rs new file mode 100644 index 00000000..ff4aba2a --- /dev/null +++ b/ruma-client-api/src/r0/media/get_media_config.rs @@ -0,0 +1,25 @@ +//! [GET /_matrix/media/r0/config](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-media-r0-config) + +use js_int::UInt; +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Gets the config for the media repository.", + method: GET, + path: "/_matrix/media/r0/config", + name: "get_media_config", + rate_limited: true, + requires_authentication: true, + } + + request {} + + response { + /// Maximum size of upload in bytes. + #[serde(rename = "m.upload.size")] + pub upload_size: UInt, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/media/get_media_preview.rs b/ruma-client-api/src/r0/media/get_media_preview.rs new file mode 100644 index 00000000..ea96ccd6 --- /dev/null +++ b/ruma-client-api/src/r0/media/get_media_preview.rs @@ -0,0 +1,73 @@ +//! [GET /_matrix/media/r0/preview_url](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-media-r0-preview-url) + +use std::time::SystemTime; + +use ruma_api::ruma_api; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Get a preview for a URL.", + name: "get_media_preview", + method: GET, + path: "/_matrix/media/r0/preview_url", + rate_limited: true, + requires_authentication: true, + } + + request { + /// URL to get a preview of. + #[ruma_api(query)] + pub url: String, + + /// Preferred point in time (in milliseconds) to return a preview for. + #[ruma_api(query)] + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub ts: SystemTime, + } + + response { + /// OpenGraph-like data for the URL. + /// + /// Differences from OpenGraph: the image size in bytes is added to the `matrix:image:size` + /// field, and `og:image` returns the MXC URI to the image, if any. + #[ruma_api(body)] + pub data: Option>, + } + + error: crate::Error +} + +#[cfg(test)] +mod tests { + use serde_json::{ + from_value as from_json_value, json, + value::{to_raw_value as to_raw_json_value, RawValue as RawJsonValue}, + }; + + // Since BTreeMap> deserialization doesn't seem to + // work, test that Option works + #[test] + fn raw_json_deserialize() { + type OptRawJson = Option>; + + assert!(from_json_value::(json!(null)) + .unwrap() + .is_none()); + assert!(from_json_value::(json!("test")) + .unwrap() + .is_some()); + assert!(from_json_value::(json!({ "a": "b" })) + .unwrap() + .is_some()); + } + + // For completeness sake, make sure serialization works too + #[test] + fn raw_json_serialize() { + assert!(to_raw_json_value(&json!(null)).is_ok()); + assert!(to_raw_json_value(&json!("string")).is_ok()); + assert!(to_raw_json_value(&json!({})).is_ok()); + assert!(to_raw_json_value(&json!({ "a": "b" })).is_ok()); + } +} diff --git a/ruma-client-api/src/r0/membership.rs b/ruma-client-api/src/r0/membership.rs new file mode 100644 index 00000000..047276f0 --- /dev/null +++ b/ruma-client-api/src/r0/membership.rs @@ -0,0 +1,52 @@ +//! Endpoints for room membership. + +pub mod ban_user; +pub mod forget_room; +pub mod get_member_events; +pub mod invite_user; +pub mod join_room_by_id; +pub mod join_room_by_id_or_alias; +pub mod joined_members; +pub mod joined_rooms; +pub mod kick_user; +pub mod leave_room; +pub mod unban_user; + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::r0::thirdparty::Medium; + +/// A signature of an `m.third_party_invite` token to prove that this user owns a third party +/// identity which has been invited to the room. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ThirdPartySigned { + /// The Matrix ID of the user who issued the invite. + pub sender: String, + + /// The Matrix ID of the invitee. + pub mxid: String, + + /// The state key of the m.third_party_invite event. + pub token: String, + + /// A signatures object containing a signature of the entire signed object. + pub signatures: BTreeMap>, +} + +/// Represents third party IDs to invite to the room. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Invite3pid { + /// Hostname and port of identity server to be used for account lookups. + pub id_server: String, + + /// An access token registered with the identity server. + pub id_access_token: String, + + /// Type of third party ID. + pub medium: Medium, + + /// Third party identifier. + pub address: String, +} diff --git a/ruma-client-api/src/r0/membership/ban_user.rs b/ruma-client-api/src/r0/membership/ban_user.rs new file mode 100644 index 00000000..0da3e1a8 --- /dev/null +++ b/ruma-client-api/src/r0/membership/ban_user.rs @@ -0,0 +1,32 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/ban](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-ban) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Ban a user from a room.", + method: POST, + name: "ban_user", + path: "/_matrix/client/r0/rooms/:room_id/ban", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to kick the user from. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The user to ban. + pub user_id: UserId, + + /// The reason for banning the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/forget_room.rs b/ruma-client-api/src/r0/membership/forget_room.rs new file mode 100644 index 00000000..39644b06 --- /dev/null +++ b/ruma-client-api/src/r0/membership/forget_room.rs @@ -0,0 +1,25 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/forget](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-forget) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +ruma_api! { + metadata { + description: "Forget a room.", + method: POST, + name: "forget_room", + path: "/_matrix/client/r0/rooms/:room_id/forget", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room to forget. + #[ruma_api(path)] + pub room_id: RoomId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/get_member_events.rs b/ruma-client-api/src/r0/membership/get_member_events.rs new file mode 100644 index 00000000..4ca6686c --- /dev/null +++ b/ruma-client-api/src/r0/membership/get_member_events.rs @@ -0,0 +1,66 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-members) + +use ruma_api::ruma_api; +use ruma_events::{room::member::MemberEvent, EventJson}; +use ruma_identifiers::RoomId; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Get membership events for a room.", + method: GET, + name: "get_member_events", + path: "/_matrix/client/r0/rooms/:room_id/members", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to get the member events for. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The point in time (pagination token) to return members for in the room. This token can + /// be obtained from a prev_batch token returned for each room by the sync API. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub at: Option, + + /// The kind of memberships to filter for. Defaults to no filtering if unspecified. When + /// specified alongside not_membership, the two parameters create an 'or' condition: either + /// the membership is the same as membership or is not the same as not_membership. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub membership: Option, + + /// The kind of memberships to *exclude* from the results. Defaults to no filtering if + /// unspecified. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub not_membership: Option, + } + + response { + /// A list of member events. + pub chunk: Vec> + } + + error: crate::Error +} + +/// The kind of membership events to filter for. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum MembershipEventFilter { + /// The user has joined. + Join, + + /// The user has been invited. + Invite, + + /// The user has left. + Leave, + + /// The user has been banned. + Ban, +} diff --git a/ruma-client-api/src/r0/membership/invite_user.rs b/ruma-client-api/src/r0/membership/invite_user.rs new file mode 100644 index 00000000..497da5d6 --- /dev/null +++ b/ruma-client-api/src/r0/membership/invite_user.rs @@ -0,0 +1,90 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/invite][invite-by-user-id] +//! +//! This endpoint has two forms: one to invite a user +//! [by their Matrix identifier][invite-by-user-id], and one to invite a user +//! [by their third party identifier][invite-by-3pid]. +//! +//! [invite-by-user-id]: https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-invite +//! [invite-by-3pid]: https://matrix.org/docs/spec/client_server/r0.6.0#id101 +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; +use serde::{Deserialize, Serialize}; + +use super::Invite3pid; + +ruma_api! { + metadata { + description: "Invite a user to a room.", + method: POST, + name: "invite_user", + path: "/_matrix/client/r0/rooms/:room_id/invite", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room where the user should be invited. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The user to invite. + #[ruma_api(body)] + pub recipient: InvitationRecipient, + } + + response {} + + error: crate::Error +} + +/// Distinguishes between invititations by Matrix or third party identifiers. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum InvitationRecipient { + /// Used to invite user by their Matrix identifer. + UserId { + /// Matrix identifier of user. + user_id: UserId, + }, + + /// Used to invite user by a third party identifer. + ThirdPartyId(Invite3pid), +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use ruma_identifiers::UserId; + use serde_json::{from_value as from_json_value, json}; + + use super::InvitationRecipient; + use crate::r0::{membership::Invite3pid, thirdparty::Medium}; + #[test] + fn deserialize_invite_by_user_id() { + let incoming = + from_json_value::(json!({ "user_id": "@carl:example.org" })) + .unwrap(); + let user_id = UserId::try_from("@carl:example.org").unwrap(); + let recipient = InvitationRecipient::UserId { user_id }; + assert_eq!(incoming, recipient); + } + + #[test] + fn deserialize_invite_by_3pid() { + let incoming = from_json_value::(json!({ + "id_server": "example.org", + "id_access_token": "abcdefghijklmnop", + "medium": "email", + "address": "carl@example.org" + })) + .unwrap(); + let recipient = InvitationRecipient::ThirdPartyId(Invite3pid { + id_server: "example.org".to_string(), + id_access_token: "abcdefghijklmnop".to_string(), + medium: Medium::Email, + address: "carl@example.org".to_string(), + }); + assert_eq!(incoming, recipient); + } +} diff --git a/ruma-client-api/src/r0/membership/join_room_by_id.rs b/ruma-client-api/src/r0/membership/join_room_by_id.rs new file mode 100644 index 00000000..340a677c --- /dev/null +++ b/ruma-client-api/src/r0/membership/join_room_by_id.rs @@ -0,0 +1,35 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/join](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-join) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +use super::ThirdPartySigned; + +ruma_api! { + metadata { + description: "Join a room using its ID.", + method: POST, + name: "join_room_by_id", + path: "/_matrix/client/r0/rooms/:room_id/join", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room where the user should be invited. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The signature of a `m.third_party_invite` token to prove that this user owns a third + /// party identity which has been invited to the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_signed: Option, + } + + response { + /// The room that the user joined. + pub room_id: RoomId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/join_room_by_id_or_alias.rs b/ruma-client-api/src/r0/membership/join_room_by_id_or_alias.rs new file mode 100644 index 00000000..91ecda4e --- /dev/null +++ b/ruma-client-api/src/r0/membership/join_room_by_id_or_alias.rs @@ -0,0 +1,41 @@ +//! [POST /_matrix/client/r0/join/{roomIdOrAlias}](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-join-roomidoralias) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, RoomIdOrAliasId}; + +use super::ThirdPartySigned; + +ruma_api! { + metadata { + description: "Join a room using its ID or one of its aliases.", + method: POST, + name: "join_room_by_id_or_alias", + path: "/_matrix/client/r0/join/:room_id_or_alias", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room where the user should be invited. + #[ruma_api(path)] + pub room_id_or_alias: RoomIdOrAliasId, + + /// The servers to attempt to join the room through. One of the servers + /// must be participating in the room. + #[ruma_api(query)] + #[serde(default)] + pub server_name: Vec, + + /// The signature of a `m.third_party_invite` token to prove that this user owns a third + /// party identity which has been invited to the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_signed: Option, + } + + response { + /// The room that the user joined. + pub room_id: RoomId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/joined_members.rs b/ruma-client-api/src/r0/membership/joined_members.rs new file mode 100644 index 00000000..dfd92cbc --- /dev/null +++ b/ruma-client-api/src/r0/membership/joined_members.rs @@ -0,0 +1,44 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/joined_members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-joined-members) + +use std::collections::BTreeMap; + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Get a map of user ids to member info objects for members of the room. Primarily for use in Application Services.", + method: GET, + name: "joined_members", + path: "/_matrix/client/r0/rooms/:room_id/joined_members", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to get the members of. + #[ruma_api(path)] + pub room_id: RoomId, + } + + response { + /// A list of the rooms the user is in, i.e. + /// the ID of each room in which the user has joined membership. + pub joined: BTreeMap, + } + + error: crate::Error +} + +/// Information about a room member. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomMember { + /// The display name of the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + + /// The mxc avatar url of the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, +} diff --git a/ruma-client-api/src/r0/membership/joined_rooms.rs b/ruma-client-api/src/r0/membership/joined_rooms.rs new file mode 100644 index 00000000..f3ec413c --- /dev/null +++ b/ruma-client-api/src/r0/membership/joined_rooms.rs @@ -0,0 +1,25 @@ +//! [GET /_matrix/client/r0/joined_rooms](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-joined-rooms) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +ruma_api! { + metadata { + description: "Get a list of the user's current rooms.", + method: GET, + name: "joined_rooms", + path: "/_matrix/client/r0/joined_rooms", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// A list of the rooms the user is in, i.e. the ID of each room in + /// which the user has joined membership. + pub joined_rooms: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/kick_user.rs b/ruma-client-api/src/r0/membership/kick_user.rs new file mode 100644 index 00000000..f6c5da5b --- /dev/null +++ b/ruma-client-api/src/r0/membership/kick_user.rs @@ -0,0 +1,32 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/kick](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-kick) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Kick a user from a room.", + method: POST, + name: "kick_user", + path: "/_matrix/client/r0/rooms/:room_id/kick", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to kick the user from. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The user to kick. + pub user_id: UserId, + + /// The reason for kicking the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/leave_room.rs b/ruma-client-api/src/r0/membership/leave_room.rs new file mode 100644 index 00000000..c0227806 --- /dev/null +++ b/ruma-client-api/src/r0/membership/leave_room.rs @@ -0,0 +1,25 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/leave](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-leave) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +ruma_api! { + metadata { + description: "Leave a room.", + method: POST, + name: "leave_room", + path: "/_matrix/client/r0/rooms/:room_id/leave", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room to leave. + #[ruma_api(path)] + pub room_id: RoomId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/membership/unban_user.rs b/ruma-client-api/src/r0/membership/unban_user.rs new file mode 100644 index 00000000..b754b701 --- /dev/null +++ b/ruma-client-api/src/r0/membership/unban_user.rs @@ -0,0 +1,28 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/unban](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-unban) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Unban a user from a room.", + method: POST, + name: "unban_user", + path: "/_matrix/client/r0/rooms/:room_id/unban", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to unban the user from. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The user to unban. + pub user_id: UserId, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/message.rs b/ruma-client-api/src/r0/message.rs new file mode 100644 index 00000000..1bcdf5ae --- /dev/null +++ b/ruma-client-api/src/r0/message.rs @@ -0,0 +1,4 @@ +//! Enpoints for sending and receiving messages + +pub mod create_message_event; +pub mod get_message_events; diff --git a/ruma-client-api/src/r0/message/create_message_event.rs b/ruma-client-api/src/r0/message/create_message_event.rs new file mode 100644 index 00000000..2a7a31b5 --- /dev/null +++ b/ruma-client-api/src/r0/message/create_message_event.rs @@ -0,0 +1,50 @@ +//! [PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid) + +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::{EventId, RoomId}; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Send a message event to a room.", + method: PUT, + name: "create_message_event", + path: "/_matrix/client/r0/rooms/:room_id/send/:event_type/:txn_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to send the event to. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The type of event to send. + #[ruma_api(path)] + pub event_type: EventType, + + /// The transaction ID for this event. + /// + /// Clients should generate an ID unique across requests with the + /// same access token; it will be used by the server to ensure + /// idempotency of requests. + #[ruma_api(path)] + pub txn_id: String, + + /// The event's content. The type for this field will be updated in a + /// future release, until then you can create a value using + /// `serde_json::value::to_raw_value`. + #[ruma_api(body)] + pub data: Box, + } + + response { + /// A unique identifier for the event. + // This is not declared required in r0.6.0, but that was a bug that has now been fixed: + // https://github.com/matrix-org/matrix-doc/pull/2525 + pub event_id: EventId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/message/get_message_events.rs b/ruma-client-api/src/r0/message/get_message_events.rs new file mode 100644 index 00000000..0248c9e7 --- /dev/null +++ b/ruma-client-api/src/r0/message/get_message_events.rs @@ -0,0 +1,176 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/messages](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-messages) + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{ + collections::all::{RoomEvent, StateEvent}, + EventJson, +}; +use ruma_identifiers::RoomId; +use serde::{Deserialize, Serialize}; + +use crate::r0::filter::RoomEventFilter; + +ruma_api! { + metadata { + description: "Get message events for a room.", + method: GET, + name: "get_message_events", + path: "/_matrix/client/r0/rooms/:room_id/messages", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to get events from. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The token to start returning events from. + /// + /// This token can be obtained from a + /// prev_batch token returned for each room by the sync API, or from a start or end token + /// returned by a previous request to this endpoint. + #[ruma_api(query)] + pub from: String, + + /// The token to stop returning events at. + /// + /// This token can be obtained from a prev_batch + /// token returned for each room by the sync endpoint, or from a start or end token returned + /// by a previous request to this endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub to: Option, + + /// The direction to return events from. + #[ruma_api(query)] + pub dir: Direction, + + /// The maximum number of events to return. + /// + /// Default: 10. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + + /// A RoomEventFilter to filter returned events with. + #[ruma_api(query)] + #[serde( + with = "ruma_serde::json_string", + default, + skip_serializing_if = "Option::is_none" + )] + pub filter: Option, + } + + response { + /// The token the pagination starts from. + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, + + /// The token the pagination ends at. + #[serde(skip_serializing_if = "Option::is_none")] + pub end: Option, + + /// A list of room events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub chunk: Vec>, + + /// A list of state events relevant to showing the `chunk`. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub state: Vec>, + } + + error: crate::Error +} + +/// The direction to return events from. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum Direction { + /// Return events backwards in time from the requested `from` token. + #[serde(rename = "b")] + Backward, + + /// Return events forwards in time from the requested `from` token. + #[serde(rename = "f")] + Forward, +} + +#[cfg(test)] +mod tests { + use super::{Direction, Request}; + + use std::convert::{TryFrom, TryInto}; + + use js_int::UInt; + use ruma_identifiers::RoomId; + + use crate::r0::filter::{LazyLoadOptions, RoomEventFilter}; + + #[test] + fn test_serialize_some_room_event_filter() { + let room_id = RoomId::try_from("!roomid:example.org").unwrap(); + let filter = RoomEventFilter { + lazy_load_options: LazyLoadOptions::Enabled { + include_redundant_members: true, + }, + rooms: Some(vec![room_id.clone()]), + not_rooms: vec!["room".into(), "room2".into(), "room3".into()], + not_types: vec!["type".into()], + ..Default::default() + }; + let req = Request { + room_id, + from: "token".into(), + to: Some("token2".into()), + dir: Direction::Backward, + limit: Some(UInt::from(0u32)), + filter: Some(filter), + }; + + let request: http::Request> = req.try_into().unwrap(); + assert_eq!( + "from=token&to=token2&dir=b&limit=0&filter=%7B%22not_types%22%3A%5B%22type%22%5D%2C%22not_rooms%22%3A%5B%22room%22%2C%22room2%22%2C%22room3%22%5D%2C%22rooms%22%3A%5B%22%21roomid%3Aexample.org%22%5D%2C%22lazy_load_members%22%3Atrue%2C%22include_redundant_members%22%3Atrue%7D", + request.uri().query().unwrap() + ); + } + + #[test] + fn test_serialize_none_room_event_filter() { + let room_id = RoomId::try_from("!roomid:example.org").unwrap(); + let req = Request { + room_id, + from: "token".into(), + to: Some("token2".into()), + dir: Direction::Backward, + limit: Some(UInt::from(0u32)), + filter: None, + }; + + let request: http::Request> = req.try_into().unwrap(); + assert_eq!( + "from=token&to=token2&dir=b&limit=0", + request.uri().query().unwrap(), + ); + } + + #[test] + fn test_serialize_default_room_event_filter() { + let room_id = RoomId::try_from("!roomid:example.org").unwrap(); + let req = Request { + room_id, + from: "token".into(), + to: Some("token2".into()), + dir: Direction::Backward, + limit: Some(UInt::from(0u32)), + filter: Some(RoomEventFilter::default()), + }; + + let request: http::Request> = req.try_into().unwrap(); + assert_eq!( + "from=token&to=token2&dir=b&limit=0&filter=%7B%7D", + request.uri().query().unwrap(), + ); + } +} diff --git a/ruma-client-api/src/r0/presence.rs b/ruma-client-api/src/r0/presence.rs new file mode 100644 index 00000000..38049dda --- /dev/null +++ b/ruma-client-api/src/r0/presence.rs @@ -0,0 +1,4 @@ +//! Endpoints for user presence. + +pub mod get_presence; +pub mod set_presence; diff --git a/ruma-client-api/src/r0/presence/get_presence.rs b/ruma-client-api/src/r0/presence/get_presence.rs new file mode 100644 index 00000000..bb7184c2 --- /dev/null +++ b/ruma-client-api/src/r0/presence/get_presence.rs @@ -0,0 +1,47 @@ +//! [GET /_matrix/client/r0/presence/{userId}/status](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-presence-userid-status) + +use std::time::Duration; + +use ruma_api::ruma_api; +use ruma_events::presence::PresenceState; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Get presence status for this user.", + method: GET, + name: "get_presence", + path: "/_matrix/client/r0/presence/:user_id/status", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The user whose presence state will be retrieved. + #[ruma_api(path)] + pub user_id: UserId, + } + + response { + /// The state message for this user if one was set. + #[serde(skip_serializing_if = "Option::is_none")] + pub status_msg: Option, + + /// Whether or not the user is currently active. + #[serde(skip_serializing_if = "Option::is_none")] + pub currently_active: Option, + + /// The length of time in milliseconds since an action was performed by the user. + #[serde( + with = "ruma_serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + pub last_active_ago: Option, + + /// The user's presence state. + pub presence: PresenceState, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/presence/set_presence.rs b/ruma-client-api/src/r0/presence/set_presence.rs new file mode 100644 index 00000000..4d3636f8 --- /dev/null +++ b/ruma-client-api/src/r0/presence/set_presence.rs @@ -0,0 +1,33 @@ +//! [PUT /_matrix/client/r0/presence/{userId}/status](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-presence-userid-status) + +use ruma_api::ruma_api; +use ruma_events::presence::PresenceState; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Set presence status for this user.", + method: PUT, + name: "set_presence", + path: "/_matrix/client/r0/presence/:user_id/status", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The user whose presence state will be updated. + #[ruma_api(path)] + pub user_id: UserId, + + /// The new presence state. + pub presence: PresenceState, + + /// The status message to attach to this state. + #[serde(skip_serializing_if = "Option::is_none")] + pub status_msg: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/profile.rs b/ruma-client-api/src/r0/profile.rs new file mode 100644 index 00000000..f87c1437 --- /dev/null +++ b/ruma-client-api/src/r0/profile.rs @@ -0,0 +1,7 @@ +//! Endpoints for user profiles. + +pub mod get_avatar_url; +pub mod get_display_name; +pub mod get_profile; +pub mod set_avatar_url; +pub mod set_display_name; diff --git a/ruma-client-api/src/r0/profile/get_avatar_url.rs b/ruma-client-api/src/r0/profile/get_avatar_url.rs new file mode 100644 index 00000000..6a950c01 --- /dev/null +++ b/ruma-client-api/src/r0/profile/get_avatar_url.rs @@ -0,0 +1,29 @@ +//! [GET /_matrix/client/r0/profile/{userId}/avatar_url](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-profile-userid-avatar-url) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Get the avatar URL of a user.", + method: GET, + name: "get_avatar_url", + path: "/_matrix/client/r0/profile/:user_id/avatar_url", + rate_limited: false, + requires_authentication: false, + } + + request { + /// The user whose avatar URL will be retrieved. + #[ruma_api(path)] + pub user_id: UserId + } + + response { + /// The user's avatar URL, if set. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/profile/get_display_name.rs b/ruma-client-api/src/r0/profile/get_display_name.rs new file mode 100644 index 00000000..d864cb60 --- /dev/null +++ b/ruma-client-api/src/r0/profile/get_display_name.rs @@ -0,0 +1,29 @@ +//! [GET /_matrix/client/r0/profile/{userId}/displayname](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-profile-userid-displayname) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Get the display name of a user.", + method: GET, + name: "get_display_name", + path: "/_matrix/client/r0/profile/:user_id/displayname", + rate_limited: false, + requires_authentication: false, + } + + request { + /// The user whose display name will be retrieved. + #[ruma_api(path)] + pub user_id: UserId + } + + response { + /// The user's display name, if set. + #[serde(skip_serializing_if = "Option::is_none")] + pub displayname: Option + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/profile/get_profile.rs b/ruma-client-api/src/r0/profile/get_profile.rs new file mode 100644 index 00000000..5bdb781e --- /dev/null +++ b/ruma-client-api/src/r0/profile/get_profile.rs @@ -0,0 +1,33 @@ +//! [GET /_matrix/client/r0/profile/{userId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-profile-userid) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Get all profile information of an user.", + method: GET, + name: "get_profile", + path: "/_matrix/client/r0/profile/:user_id", + rate_limited: false, + requires_authentication: false, + } + + request { + /// The user whose profile will be retrieved. + #[ruma_api(path)] + pub user_id: UserId, + } + + response { + /// The user's avatar URL, if set. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + + /// The user's display name, if set. + #[serde(skip_serializing_if = "Option::is_none")] + pub displayname: Option, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/profile/set_avatar_url.rs b/ruma-client-api/src/r0/profile/set_avatar_url.rs new file mode 100644 index 00000000..b6d6064e --- /dev/null +++ b/ruma-client-api/src/r0/profile/set_avatar_url.rs @@ -0,0 +1,30 @@ +//! [PUT /_matrix/client/r0/profile/{userId}/avatar_url](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-profile-userid-avatar-url) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Set the avatar URL of the user.", + method: PUT, + name: "set_avatar_url", + path: "/_matrix/client/r0/profile/:user_id/avatar_url", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The user whose avatar URL will be set. + #[ruma_api(path)] + pub user_id: UserId, + + /// The new avatar URL for the user. + /// + /// `None` is used to unset the avatar. + pub avatar_url: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/profile/set_display_name.rs b/ruma-client-api/src/r0/profile/set_display_name.rs new file mode 100644 index 00000000..147740df --- /dev/null +++ b/ruma-client-api/src/r0/profile/set_display_name.rs @@ -0,0 +1,29 @@ +//! [PUT /_matrix/client/r0/profile/{userId}/displayname](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-profile-userid-displayname) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +ruma_api! { + metadata { + description: "Set the display name of the user.", + method: PUT, + name: "set_display_name", + path: "/_matrix/client/r0/profile/:user_id/displayname", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The user whose display name will be set. + #[ruma_api(path)] + pub user_id: UserId, + + /// The new display name for the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub displayname: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push.rs b/ruma-client-api/src/r0/push.rs new file mode 100644 index 00000000..e967b852 --- /dev/null +++ b/ruma-client-api/src/r0/push.rs @@ -0,0 +1,140 @@ +//! Endpoints for push notifications. + +use std::convert::TryFrom; + +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; + +pub mod delete_pushrule; +pub mod get_notifications; +pub mod get_pushers; +pub mod get_pushrule; +pub mod get_pushrule_actions; +pub mod get_pushrule_enabled; +pub mod get_pushrules_all; +pub mod get_pushrules_global_scope; +pub mod set_pusher; +pub mod set_pushrule; +pub mod set_pushrule_actions; +pub mod set_pushrule_enabled; + +pub use ruma_common::push::Action; +pub use ruma_events::push_rules::PushCondition; + +/// The kinds of push rules that are available +#[derive( + Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Display, EnumString, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RuleKind { + /// User-configured rules that override all other kinds + Override, + + /// Lowest priority user-defined rules + Underride, + + /// Sender-specific rules + Sender, + + /// Room-specific rules + Room, + + /// Content-specific rules + Content, +} + +impl TryFrom<&'_ str> for RuleKind { + type Error = strum::ParseError; + + fn try_from(s: &str) -> Result { + s.parse() + } +} + +/// A push rule +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PushRule { + /// The actions to perform when this rule is matched. + pub actions: Vec, + + /// Whether this is a default rule, or has been set explicitly. + pub default: bool, + + /// Whether the push rule is enabled or not. + pub enabled: bool, + + /// The ID of this rule. + pub rule_id: String, + + /// The conditions that must hold true for an event in order for a rule to be applied to an event. A rule with no conditions always matches. + /// Only applicable to underride and override rules. + #[serde(skip_serializing_if = "Option::is_none")] + pub conditions: Option>, + + /// The glob-style pattern to match against. Only applicable to content rules. + #[serde(skip_serializing_if = "Option::is_none")] + pub pattern: Option, +} + +/// Defines a pusher +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Pusher { + /// This is a unique identifier for this pusher. Max length, 512 bytes. + pub pushkey: String, + + /// The kind of the pusher. If set to None in a call to set_pusher, this + /// will delete the pusher + pub kind: Option, + + /// This is a reverse-DNS style identifier for the application. Max length, 64 chars. + pub app_id: String, + + /// A string that will allow the user to identify what application owns this pusher. + pub app_display_name: String, + + /// A string that will allow the user to identify what device owns this pusher. + pub device_display_name: String, + + /// This string determines which set of device specific rules this pusher executes. + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_tag: Option, + + /// The preferred language for receiving notifications (e.g. 'en' or 'en-US') + pub lang: String, + + /// Information for the pusher implementation itself. + pub data: PusherData, +} + +/// Which kind a pusher is +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PusherKind { + /// A pusher that sends HTTP pokes. + Http, + + /// A pusher that emails the user with unread notifications. + Email, +} + +/// Information for the pusher implementation itself. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PusherData { + /// Required if the pusher's kind is http. The URL to use to send notifications to. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + + /// The format to use when sending notifications to the Push Gateway. + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, +} + +/// A special format that the homeserver should use when sending notifications to a Push Gateway. +/// Currently, only "event_id_only" is supported as of [Push Gateway API r0.1.1](https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour) +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PushFormat { + /// Require the homeserver to only send a reduced set of fields in the push. + EventIdOnly, +} diff --git a/ruma-client-api/src/r0/push/delete_pushrule.rs b/ruma-client-api/src/r0/push/delete_pushrule.rs new file mode 100644 index 00000000..a5a35377 --- /dev/null +++ b/ruma-client-api/src/r0/push/delete_pushrule.rs @@ -0,0 +1,34 @@ +//! [DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}](https://matrix.org/docs/spec/client_server/r0.6.0#delete-matrix-client-r0-pushrules-scope-kind-ruleid) + +use ruma_api::ruma_api; + +use super::RuleKind; + +ruma_api! { + metadata { + description: "This endpoint removes the push rule defined in the path.", + method: DELETE, + name: "delete_pushrule", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The scope to delete from. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/get_notifications.rs b/ruma-client-api/src/r0/push/get_notifications.rs new file mode 100644 index 00000000..43a946c3 --- /dev/null +++ b/ruma-client-api/src/r0/push/get_notifications.rs @@ -0,0 +1,77 @@ +//! [GET /_matrix/client/r0/notifications](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-notifications) + +use std::time::SystemTime; + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{collections::all, EventJson}; +use ruma_identifiers::RoomId; +use serde::{Deserialize, Serialize}; + +use super::Action; + +ruma_api! { + metadata { + description: "Paginate through the list of events that the user has been, or would have been notified about.", + method: GET, + name: "get_notifications", + path: "/_matrix/client/r0/notifications", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Pagination token given to retrieve the next set of events. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + /// Limit on the number of events to return in this request. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// Allows basic filtering of events returned. Supply "highlight" to return only events where + /// the notification had the 'highlight' tweak set. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub only: Option + } + + response { + /// The token to supply in the from param of the next /notifications request in order + /// to request more events. If this is absent, there are no more results. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_token: Option, + + + /// The list of events that triggered notifications. + pub notifications: Vec>, + } + + error: crate::Error +} + +/// Represents a notification +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Notification { + /// The actions to perform when the conditions for this rule are met. + pub actions: Vec, + + /// The event that triggered the notification. + pub event: EventJson, + + /// The profile tag of the rule that matched this event. + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_tag: Option, + + /// Indicates whether the user has sent a read receipt indicating that they have read this message. + pub read: bool, + + /// The ID of the room in which the event was posted. + pub room_id: RoomId, + + /// The time at which the event notification was sent, in milliseconds. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub ts: SystemTime, +} diff --git a/ruma-client-api/src/r0/push/get_pushers.rs b/ruma-client-api/src/r0/push/get_pushers.rs new file mode 100644 index 00000000..729a62e7 --- /dev/null +++ b/ruma-client-api/src/r0/push/get_pushers.rs @@ -0,0 +1,25 @@ +//! [GET /_matrix/client/r0/pushers](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-pushers) + +use ruma_api::ruma_api; + +use super::Pusher; + +ruma_api! { + metadata { + description: "Gets all currently active pushers for the authenticated user.", + method: GET, + name: "get_pushers", + path: "/_matrix/client/r0/pushers", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// An array containing the current pushers for the user. + pub pushers: Vec + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/get_pushrule.rs b/ruma-client-api/src/r0/push/get_pushrule.rs new file mode 100644 index 00000000..482e8dd5 --- /dev/null +++ b/ruma-client-api/src/r0/push/get_pushrule.rs @@ -0,0 +1,38 @@ +//! [GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-pushrules-scope-kind-ruleid) + +use ruma_api::ruma_api; + +use super::{PushRule, RuleKind}; + +ruma_api! { + metadata { + description: "Retrieve a single specified push rule.", + method: GET, + name: "get_pushrule", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The scope to fetch rules from. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + } + + response { + /// The specific push rule. + #[ruma_api(body)] + pub rule: PushRule + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/get_pushrule_actions.rs b/ruma-client-api/src/r0/push/get_pushrule_actions.rs new file mode 100644 index 00000000..0fd7b048 --- /dev/null +++ b/ruma-client-api/src/r0/push/get_pushrule_actions.rs @@ -0,0 +1,37 @@ +//! [GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-pushrules-scope-kind-ruleid-actions) + +use ruma_api::ruma_api; + +use super::{Action, RuleKind}; + +ruma_api! { + metadata { + description: "This endpoint get the actions for the specified push rule.", + method: GET, + name: "get_pushrule_actions", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id/actions", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The scope to fetch a rule from. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + } + + response { + /// The actions to perform for this rule. + pub actions: Vec + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/get_pushrule_enabled.rs b/ruma-client-api/src/r0/push/get_pushrule_enabled.rs new file mode 100644 index 00000000..ae9fcfbf --- /dev/null +++ b/ruma-client-api/src/r0/push/get_pushrule_enabled.rs @@ -0,0 +1,37 @@ +//! [GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-pushrules-scope-kind-ruleid-enabled) + +use ruma_api::ruma_api; + +use super::RuleKind; + +ruma_api! { + metadata { + description: "This endpoint gets whether the specified push rule is enabled.", + method: GET, + name: "get_pushrule_enabled", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id/enabled", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The scope to fetch a rule from. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + } + + response { + /// Whether the push rule is enabled or not. + pub enabled: bool + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/get_pushrules_all.rs b/ruma-client-api/src/r0/push/get_pushrules_all.rs new file mode 100644 index 00000000..cb6feaa4 --- /dev/null +++ b/ruma-client-api/src/r0/push/get_pushrules_all.rs @@ -0,0 +1,24 @@ +//! [GET /_matrix/client/r0/pushrules/](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-pushrules) + +use ruma_api::ruma_api; +use ruma_events::push_rules::Ruleset; + +ruma_api! { + metadata { + description: "Retrieve all push rulesets for this user.", + method: GET, + name: "get_pushrules_all", + path: "/_matrix/client/r0/pushrules/", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// The global ruleset + pub global: Ruleset, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/get_pushrules_global_scope.rs b/ruma-client-api/src/r0/push/get_pushrules_global_scope.rs new file mode 100644 index 00000000..eedcce43 --- /dev/null +++ b/ruma-client-api/src/r0/push/get_pushrules_global_scope.rs @@ -0,0 +1,25 @@ +//! [GET /_matrix/client/r0/pushrules/global/](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-pushrules) + +use ruma_api::ruma_api; +use ruma_events::push_rules::Ruleset; + +ruma_api! { + metadata { + description: "Retrieve all push rulesets in the global scope for this user.", + method: GET, + name: "get_pushrules_global_scope", + path: "/_matrix/client/r0/pushrules/global/", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// The global ruleset. + #[ruma_api(body)] + pub global: Ruleset, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/set_pusher.rs b/ruma-client-api/src/r0/push/set_pusher.rs new file mode 100644 index 00000000..17c90599 --- /dev/null +++ b/ruma-client-api/src/r0/push/set_pusher.rs @@ -0,0 +1,32 @@ +//! [POST /_matrix/client/r0/pushers/set](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-pushers-set) + +use ruma_api::ruma_api; + +use super::Pusher; + +ruma_api! { + metadata { + description: "This endpoint allows the creation, modification and deletion of pushers for this user ID.", + method: POST, + name: "set_pusher", + path: "/_matrix/client/r0/pushers/set", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The pusher to configure + #[serde(flatten)] + pub pusher: Pusher, + + /// Controls if another pusher with the same pushkey and app id should be created. + /// See the spec for details. + #[serde(default)] + pub append: bool + + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/set_pushrule.rs b/ruma-client-api/src/r0/push/set_pushrule.rs new file mode 100644 index 00000000..f1066c3f --- /dev/null +++ b/ruma-client-api/src/r0/push/set_pushrule.rs @@ -0,0 +1,54 @@ +//! [PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-pushrules-scope-kind-ruleid) + +use ruma_api::ruma_api; + +use super::{Action, PushCondition, RuleKind}; + +ruma_api! { + metadata { + description: "This endpoint allows the creation, modification and deletion of pushers for this user ID.", + method: PUT, + name: "set_pushrule", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The scope to set the rule in. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + + /// Use 'before' with a rule_id as its value to make the new rule the next-most important rule with respect to the given user defined rule. + #[ruma_api(query)] + pub before: Option, + + /// This makes the new rule the next-less important rule relative to the given user defined rule. + #[ruma_api(query)] + pub after: Option, + + /// The actions to perform when this rule is matched. + pub actions: Vec, + + /// 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. + /// Only applicable to underride and override rules, empty Vec otherwise. + #[serde(default)] + pub conditions: Vec, + + /// The glob-style pattern to match against. Only applicable to content rules. + #[serde(skip_serializing_if = "Option::is_none")] + pub pattern: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/set_pushrule_actions.rs b/ruma-client-api/src/r0/push/set_pushrule_actions.rs new file mode 100644 index 00000000..d3088b63 --- /dev/null +++ b/ruma-client-api/src/r0/push/set_pushrule_actions.rs @@ -0,0 +1,37 @@ +//! [PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-pushrules-scope-kind-ruleid-actions) + +use ruma_api::ruma_api; + +use super::{Action, RuleKind}; + +ruma_api! { + metadata { + description: "This endpoint allows clients to change the actions of a push rule. This can be used to change the actions of builtin rules.", + method: PUT, + name: "set_pushrule_actions", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id/actions", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The scope to fetch a rule from. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + + /// The actions to perform for this rule + pub actions: Vec + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/push/set_pushrule_enabled.rs b/ruma-client-api/src/r0/push/set_pushrule_enabled.rs new file mode 100644 index 00000000..d0a04002 --- /dev/null +++ b/ruma-client-api/src/r0/push/set_pushrule_enabled.rs @@ -0,0 +1,37 @@ +//! [PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-pushrules-scope-kind-ruleid-enabled) + +use ruma_api::ruma_api; + +use super::RuleKind; + +ruma_api! { + metadata { + description: "This endpoint allows clients to enable or disable the specified push rule.", + method: PUT, + name: "set_pushrule_enabled", + path: "/_matrix/client/r0/pushrules/:scope/:kind/:rule_id/enabled", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The scope to fetch a rule from. 'global' to specify global rules. + #[ruma_api(path)] + pub scope: String, + + /// The kind of rule + #[ruma_api(path)] + pub kind: RuleKind, + + /// The identifier for the rule. + #[ruma_api(path)] + pub rule_id: String, + + /// Whether the push rule is enabled or not. + pub enabled: bool + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/read_marker.rs b/ruma-client-api/src/r0/read_marker.rs new file mode 100644 index 00000000..7b9e14c3 --- /dev/null +++ b/ruma-client-api/src/r0/read_marker.rs @@ -0,0 +1,3 @@ +//! Endpoints for "fully read markers" + +pub mod set_read_marker; diff --git a/ruma-client-api/src/r0/read_marker/set_read_marker.rs b/ruma-client-api/src/r0/read_marker/set_read_marker.rs new file mode 100644 index 00000000..5ef38743 --- /dev/null +++ b/ruma-client-api/src/r0/read_marker/set_read_marker.rs @@ -0,0 +1,37 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/read_markers](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-read-markers) + +use ruma_api::ruma_api; +use ruma_identifiers::{EventId, RoomId}; + +ruma_api! { + metadata { + description: "Sets the position of the read marker for a given room, and optionally the read receipt's location.", + method: POST, + name: "set_read_marker", + path: "/_matrix/client/r0/rooms/:room_id/read_markers", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room ID to set the read marker in for the user. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The event ID the read marker should be located at. + /// The event MUST belong to the room. + #[serde(rename = "m.fully_read")] + pub fully_read: EventId, + + /// The event ID to set the read receipt location at. + /// This is equivalent to calling the create_read_receipt endpoint and is + /// provided here to save that extra call. + #[serde(rename = "m.read", skip_serializing_if = "Option::is_none")] + pub read_receipt: Option, + + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/receipt.rs b/ruma-client-api/src/r0/receipt.rs new file mode 100644 index 00000000..ccb1edc3 --- /dev/null +++ b/ruma-client-api/src/r0/receipt.rs @@ -0,0 +1,3 @@ +//! Endpoints for event receipts. + +pub mod create_receipt; diff --git a/ruma-client-api/src/r0/receipt/create_receipt.rs b/ruma-client-api/src/r0/receipt/create_receipt.rs new file mode 100644 index 00000000..0b4b25e7 --- /dev/null +++ b/ruma-client-api/src/r0/receipt/create_receipt.rs @@ -0,0 +1,52 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-receipt-receipttype-eventid) + +use std::convert::TryFrom; + +use ruma_api::ruma_api; +use ruma_identifiers::{EventId, RoomId}; +use strum::{Display, EnumString}; + +ruma_api! { + metadata { + description: "Send a receipt event to a room.", + method: POST, + name: "create_receipt", + path: "/_matrix/client/r0/rooms/:room_id/receipt/:receipt_type/:event_id", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The room in which to send the event. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The type of receipt to send. + #[ruma_api(path)] + pub receipt_type: ReceiptType, + + /// The event ID to acknowledge up to. + #[ruma_api(path)] + pub event_id: EventId, + } + + response {} + + error: crate::Error +} + +/// The type of receipt. +#[derive(Clone, Copy, Debug, Display, EnumString)] +pub enum ReceiptType { + /// m.read + #[strum(serialize = "m.read")] + Read, +} + +impl TryFrom<&'_ str> for ReceiptType { + type Error = strum::ParseError; + + fn try_from(s: &str) -> Result { + s.parse() + } +} diff --git a/ruma-client-api/src/r0/redact.rs b/ruma-client-api/src/r0/redact.rs new file mode 100644 index 00000000..e43d631f --- /dev/null +++ b/ruma-client-api/src/r0/redact.rs @@ -0,0 +1,3 @@ +//! Endpoints for event redaction. + +pub mod redact_event; diff --git a/ruma-client-api/src/r0/redact/redact_event.rs b/ruma-client-api/src/r0/redact/redact_event.rs new file mode 100644 index 00000000..30bc294b --- /dev/null +++ b/ruma-client-api/src/r0/redact/redact_event.rs @@ -0,0 +1,42 @@ +//! [PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid) + +use ruma_api::ruma_api; +use ruma_identifiers::{EventId, RoomId}; + +ruma_api! { + metadata { + description: "Redact an event, stripping all information not critical to the event graph integrity.", + method: PUT, + name: "redact_event", + path: "/_matrix/client/r0/rooms/:room_id/redact/:event_id/:txn_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The ID of the room of the event to redact. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The ID of the event to redact. + #[ruma_api(path)] + pub event_id: EventId, + + /// The transaction ID for this event. + /// + /// Clients should generate a unique ID; it will be used by the server to ensure idempotency of requests. + #[ruma_api(path)] + pub txn_id: String, + + /// The reason for the redaction. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + } + + response { + /// The ID of the redacted event. + pub event_id: EventId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/room.rs b/ruma-client-api/src/r0/room.rs new file mode 100644 index 00000000..6c20c58b --- /dev/null +++ b/ruma-client-api/src/r0/room.rs @@ -0,0 +1,19 @@ +//! Endpoints for room management. + +pub mod create_room; +pub mod get_room_event; +pub mod report_content; +pub mod upgrade_room; + +use serde::{Deserialize, Serialize}; + +/// Whether or not a newly created room will be listed in the room directory. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Visibility { + /// Indicates that the room will be shown in the published room list. + Public, + + /// Indicates that the room will not be shown in the published room list. + Private, +} diff --git a/ruma-client-api/src/r0/room/create_room.rs b/ruma-client-api/src/r0/room/create_room.rs new file mode 100644 index 00000000..f69cb0bb --- /dev/null +++ b/ruma-client-api/src/r0/room/create_room.rs @@ -0,0 +1,164 @@ +//! [POST /_matrix/client/r0/createRoom](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-createroom) + +use ruma_api::ruma_api; +use ruma_events::{ + room::{ + create::{CreateEventContent, PreviousRoom}, + power_levels::PowerLevelsEventContent, + }, + EventJson, EventType, +}; +use ruma_identifiers::{RoomId, RoomVersionId, UserId}; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; + +use super::Visibility; +use crate::r0::membership::Invite3pid; + +ruma_api! { + metadata { + description: "Create a new room.", + method: POST, + name: "create_room", + path: "/_matrix/client/r0/createRoom", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Extra keys to be added to the content of the `m.room.create`. + #[serde(skip_serializing_if = "Option::is_none")] + pub creation_content: Option, + + /// List of state events to send to the new room. + /// + /// Takes precedence over events set by preset, but gets overriden by + /// name and topic keys. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub initial_state: Vec, + + /// A list of user IDs to invite to the room. + /// + /// This will tell the server to invite everyone in the list to the newly created room. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub invite: Vec, + + /// List of third party IDs of users to invite. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub invite_3pid: Vec, + + /// If set, this sets the `is_direct` flag on room invites. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_direct: Option, + + /// If this is included, an `m.room.name` event will be sent into the room to indicate + /// the name of the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Power level content to override in the default power level event. + #[serde(skip_serializing_if = "Option::is_none")] + pub power_level_content_override: Option>, + + /// Convenience parameter for setting various default state events based on a preset. + #[serde(skip_serializing_if = "Option::is_none")] + pub preset: Option, + + /// The desired room alias local part. + #[serde(skip_serializing_if = "Option::is_none")] + pub room_alias_name: Option, + + /// Room version to set for the room. Defaults to homeserver's default if not specified. + #[serde(skip_serializing_if = "Option::is_none")] + pub room_version: Option, + + /// If this is included, an `m.room.topic` event will be sent into the room to indicate + /// the topic for the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + + /// A public visibility indicates that the room will be shown in the published room + /// list. A private visibility will hide the room from the published room list. Rooms + /// default to private visibility if this key is not included. + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, + } + + response { + /// The created room's ID. + pub room_id: RoomId, + } + + error: crate::Error +} + +/// Extra options to be added to the `m.room.create` event. +/// +/// This is the same as the event content struct for `m.room.create`, but without some fields that +/// servers are supposed to ignore. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CreationContent { + /// Whether users on other servers can join this room. + /// + /// Defaults to `true` if key does not exist. + #[serde( + rename = "m.federate", + default = "ruma_serde::default_true", + skip_serializing_if = "ruma_serde::is_true" + )] + pub federate: bool, + /// A reference to the room this room replaces, if the previous room was upgraded. + #[serde(skip_serializing_if = "Option::is_none")] + pub predecessor: Option, +} + +impl CreationContent { + /// Given a `CreationContent` and the other fields that a homeserver has to fill, construct + /// a `CreateEventContent`. + pub fn into_event_content( + Self { + federate, + predecessor, + }: Self, + creator: UserId, + room_version: RoomVersionId, + ) -> CreateEventContent { + CreateEventContent { + creator, + federate, + room_version, + predecessor, + } + } +} + +/// A convenience parameter for setting a few default state events. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RoomPreset { + /// `join_rules` is set to `invite` and `history_visibility` is set to `shared`. + PrivateChat, + + /// `join_rules` is set to `public` and `history_visibility` is set to `shared`. + PublicChat, + + /// Same as `PrivateChat`, but all initial invitees get the same power level as the creator. + TrustedPrivateChat, +} + +/// Represents content of a state event to be used to initalize new room state. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InitialStateEvent { + /// State event type. + // FIXME: This should really only be a subset of the EventType enum. + #[serde(rename = "type")] + pub event_type: EventType, + + /// `state_key` of the event to be sent. + pub state_key: Option, + + /// JSON content of the state event. + /// + /// To create a `Box`, use `serde_json::value::to_raw_value`. + pub content: Box, +} diff --git a/ruma-client-api/src/r0/room/get_room_event.rs b/ruma-client-api/src/r0/room/get_room_event.rs new file mode 100644 index 00000000..fdf58126 --- /dev/null +++ b/ruma-client-api/src/r0/room/get_room_event.rs @@ -0,0 +1,33 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-event-eventid) + +use ruma_api::ruma_api; +use ruma_events::{collections::all, EventJson}; +use ruma_identifiers::{EventId, RoomId}; + +ruma_api! { + metadata { + description: "Get a single event based on roomId/eventId", + method: GET, + name: "get_room_event", + path: "/_matrix/client/r0/rooms/:room_id/event/:event_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The ID of the room the event is in. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The ID of the event. + #[ruma_api(path)] + pub event_id: EventId, + } + + response { + /// Arbitrary JSON of the event body. Returns both room and state events. + pub event: EventJson, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/room/report_content.rs b/ruma-client-api/src/r0/room/report_content.rs new file mode 100644 index 00000000..690e3d03 --- /dev/null +++ b/ruma-client-api/src/r0/room/report_content.rs @@ -0,0 +1,36 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/report/{eventId}](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-report-eventid) + +use js_int::Int; +use ruma_api::ruma_api; +use ruma_identifiers::{EventId, RoomId}; + +ruma_api! { + metadata { + description: "Report content as inappropriate.", + method: POST, + name: "report_content", + path: "/rooms/:room_id/report/:event_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Room in which the event to be reported is located. + #[ruma_api(path)] + pub room_id: RoomId, + + /// Event to report. + #[ruma_api(path)] + pub event_id: EventId, + + /// Integer between -100 and 0 rating offensivness. + pub score: Int, + + /// Reason to report content. May be blank. + pub reason: String, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/room/upgrade_room.rs b/ruma-client-api/src/r0/room/upgrade_room.rs new file mode 100644 index 00000000..58e7e925 --- /dev/null +++ b/ruma-client-api/src/r0/room/upgrade_room.rs @@ -0,0 +1,31 @@ +//! [POST /_matrix/client/r0/rooms/{roomId}/upgrade](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-rooms-roomid-upgrade) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomId; + +ruma_api! { + metadata { + description: "Upgrades a room to a particular version.", + method: POST, + name: "upgrade_room", + path: "/_matrix/client/r0/rooms/:room_id/upgrade", + rate_limited: false, + requires_authentication: true, + } + + request { + /// ID of the room to be upgraded. + #[ruma_api(path)] + pub room_id: RoomId, + + /// New version for the room. + pub new_version: String, + } + + response { + /// ID of the new room. + pub replacement_room: RoomId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/search.rs b/ruma-client-api/src/r0/search.rs new file mode 100644 index 00000000..88ced6cc --- /dev/null +++ b/ruma-client-api/src/r0/search.rs @@ -0,0 +1,3 @@ +//! Endpoints for event searches. + +pub mod search_events; diff --git a/ruma-client-api/src/r0/search/search_events.rs b/ruma-client-api/src/r0/search/search_events.rs new file mode 100644 index 00000000..1e07ff62 --- /dev/null +++ b/ruma-client-api/src/r0/search/search_events.rs @@ -0,0 +1,282 @@ +//! [POST /_matrix/client/r0/search](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-search) + +use std::collections::BTreeMap; + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{ + collections::all::{Event, StateEvent}, + EventJson, +}; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde::{Deserialize, Serialize}; + +use crate::r0::filter::RoomEventFilter; + +ruma_api! { + metadata { + description: "Search events.", + method: POST, + name: "search", + path: "/_matrix/client/r0/search", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The point to return events from. + /// + /// If given, this should be a `next_batch` result from a previous call to this endpoint. + #[ruma_api(query)] + pub next_batch: Option, + + /// Describes which categories to search in and their criteria. + pub search_categories: Categories, + } + + response { + /// A grouping of search results by category. + pub search_categories: ResultCategories, + } + + error: crate::Error +} + +/// Categories of events that can be searched for. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Categories { + /// Criteria for searching a category of events. + #[serde(skip_serializing_if = "Option::is_none")] + pub room_events: Option, +} + +/// Criteria for searching a category of events. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Criteria { + /// The string to search events for. + pub search_term: String, + + /// The keys to search for. Defaults to all keys. + #[serde(skip_serializing_if = "Option::is_none")] + pub keys: Option>, + + /// A `Filter` to apply to the search. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option, + + /// The order in which to search for results. + #[serde(skip_serializing_if = "Option::is_none")] + pub order_by: Option, + + /// Configures whether any context for the events returned are included in the response. + #[serde(skip_serializing_if = "Option::is_none")] + pub event_context: Option, + + /// Requests the server return the current state for each room returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_state: Option, + + /// Requests that the server partitions the result set based on the provided list of keys. + #[serde(skip_serializing_if = "Option::is_none")] + pub groupings: Option, +} + +/// Configures whether any context for the events returned are included in the response. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct EventContext { + /// How many events before the result are returned. + #[serde( + default = "default_event_context_limit", + skip_serializing_if = "is_default_event_context_limit" + )] + pub before_limit: UInt, + + /// How many events after the result are returned. + #[serde( + default = "default_event_context_limit", + skip_serializing_if = "is_default_event_context_limit" + )] + pub after_limit: UInt, + + /// Requests that the server returns the historic profile information for the users that + /// sent the events that were returned. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub include_profile: bool, +} + +fn default_event_context_limit() -> UInt { + UInt::from(5u32) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_default_event_context_limit(val: &UInt) -> bool { + *val == default_event_context_limit() +} + +/// Context for search results, if requested. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EventContextResult { + /// Pagination token for the end of the chunk. + #[serde(skip_serializing_if = "Option::is_none")] + pub end: Option, + + /// Events just after the result. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events_after: Vec>, + + /// Events just before the result. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events_before: Vec>, + + /// The historic profile information of the users that sent the events returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_info: Option>, + + /// Pagination token for the start of the chunk. + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, +} + +/// A grouping for partioning the result set. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct Grouping { + /// The key within events to use for this grouping. + pub key: Option, +} + +/// The key within events to use for this grouping. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum GroupingKey { + /// `room_id` + RoomId, + + /// `sender` + Sender, +} + +/// Requests that the server partitions the result set based on the provided list of keys. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Groupings { + /// List of groups to request. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub group_by: Vec, +} + +/// The keys to search for. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum SearchKeys { + /// content.body + #[serde(rename = "content.body")] + ContentBody, + + /// content.name + #[serde(rename = "content.name")] + ContentName, + + /// content.topic + #[serde(rename = "content.topic")] + ContentTopic, +} + +/// The order in which to search for results. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum OrderBy { + /// Prioritize recent events. + Recent, + + /// Prioritize events by a numerical ranking of how closely they matched the search + /// criteria. + Rank, +} + +/// Categories of events that can be searched for. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ResultCategories { + /// Room event results. + #[serde(skip_serializing_if = "Option::is_none")] + pub room_events: Option, +} + +/// Categories of events that can be searched for. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomEventJsons { + /// An approximate count of the total number of results found. + pub count: UInt, + + /// Any groups that were requested. + pub groups: BTreeMap>, + + /// Token that can be used to get the next batch of results, by passing as the `next_batch` + /// parameter to the next call. If this field is absent, there are no more results. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_batch: Option, + + /// List of results in the requested order. + pub results: Vec, + + /// The current state for every room in the results. This is included if the request had the + /// `include_state` key set with a value of `true`. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub state: BTreeMap>>, + + /// List of words which should be highlighted, useful for stemming which may + /// change the query terms. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub highlights: Vec, +} + +/// A grouping of results, if requested. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ResultGroup { + /// Token that can be used to get the next batch of results in the group, by passing as the + /// `next_batch` parameter to the next call. If this field is absent, there are no more + /// results in this group. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_batch: Option, + + /// Key that can be used to order different groups. + pub order: UInt, + + /// Which results are in this group. + pub results: Vec, +} + +/// A search result. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SearchResult { + /// Context for result, if requested. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + + /// A number that describes how closely this result matches the search. Higher is closer. + #[serde(skip_serializing_if = "Option::is_none")] + pub rank: Option, + + /// The event that matched. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +/// A user profile. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UserProfile { + /// The user's avatar URL, if set. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + + /// The user's display name, if set. + #[serde(skip_serializing_if = "Option::is_none")] + pub displayname: Option, +} + +/// Represents either a room or user ID for returning grouped search results. +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub enum RoomIdOrUserId { + /// Represents a room ID. + RoomId(RoomId), + + /// Represents a user ID. + UserId(UserId), +} diff --git a/ruma-client-api/src/r0/server.rs b/ruma-client-api/src/r0/server.rs new file mode 100644 index 00000000..9d523d0a --- /dev/null +++ b/ruma-client-api/src/r0/server.rs @@ -0,0 +1,3 @@ +//! Endpoints for server administration. + +pub mod get_user_info; diff --git a/ruma-client-api/src/r0/server/get_user_info.rs b/ruma-client-api/src/r0/server/get_user_info.rs new file mode 100644 index 00000000..4f00ddd8 --- /dev/null +++ b/ruma-client-api/src/r0/server/get_user_info.rs @@ -0,0 +1,66 @@ +//! [GET /_matrix/client/r0/admin/whois/{userId}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-admin-whois-userid) + +use std::{collections::BTreeMap, time::SystemTime}; + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Get information about a particular user.", + method: GET, + name: "get_user_info", + path: "/_matrix/client/r0/admin/whois/:user_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The user to look up. + #[ruma_api(path)] + pub user_id: UserId, + } + + response { + /// The Matrix user ID of the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + + /// A map of the user's device identifiers to information about that device. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub devices: BTreeMap, + } + + error: crate::Error +} + +/// Information about a user's device. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DeviceInfo { + /// A list of user sessions on this device. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sessions: Vec, +} + +/// Information about a user session. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SessionInfo { + /// A list of connections in this session. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub connections: Vec, +} + +/// Information about a connection in a user session. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConnectionInfo { + /// Most recently seen IP address of the session. + pub ip: Option, + + /// Time when that the session was last active. + #[serde(with = "ruma_serde::time::opt_ms_since_unix_epoch")] + pub last_seen: Option, + + /// User agent string last seen in the session. + pub user_agent: Option, +} diff --git a/ruma-client-api/src/r0/session.rs b/ruma-client-api/src/r0/session.rs new file mode 100644 index 00000000..e1df60b8 --- /dev/null +++ b/ruma-client-api/src/r0/session.rs @@ -0,0 +1,7 @@ +//! Endpoints for user session management. + +pub mod get_login_types; +pub mod login; +pub mod logout; +pub mod logout_all; +pub mod sso_login; diff --git a/ruma-client-api/src/r0/session/get_login_types.rs b/ruma-client-api/src/r0/session/get_login_types.rs new file mode 100644 index 00000000..0e30d789 --- /dev/null +++ b/ruma-client-api/src/r0/session/get_login_types.rs @@ -0,0 +1,52 @@ +//! [GET /_matrix/client/r0/login](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-login) + +use ruma_api::ruma_api; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Gets the homeserver's supported login types to authenticate users. Clients should pick one of these and supply it as the type when logging in.", + method: GET, + name: "get_login_types", + path: "/_matrix/client/r0/login", + rate_limited: true, + requires_authentication: false, + } + + request {} + + response { + /// The homeserver's supported login types. + pub flows: Vec + } + + error: crate::Error +} + +/// An authentication mechanism. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum LoginType { + /// A password is supplied to authenticate. + #[serde(rename = "m.login.password")] + Password, + + /// Token-based login. + #[serde(rename = "m.login.token")] + Token, +} + +#[cfg(test)] +mod tests { + use serde_json::{from_value as from_json_value, json}; + + use super::LoginType; + + #[test] + fn deserialize_login_type() { + assert_eq!( + from_json_value::(json!({ "type": "m.login.password" })).unwrap(), + LoginType::Password, + ); + } +} diff --git a/ruma-client-api/src/r0/session/login.rs b/ruma-client-api/src/r0/session/login.rs new file mode 100644 index 00000000..7c0216b2 --- /dev/null +++ b/ruma-client-api/src/r0/session/login.rs @@ -0,0 +1,220 @@ +//! [POST /_matrix/client/r0/login](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-login) + +use ruma_api::ruma_api; +use ruma_identifiers::{DeviceId, UserId}; +use serde::{Deserialize, Serialize}; + +use crate::r0::thirdparty::Medium; + +ruma_api! { + metadata { + description: "Login to the homeserver.", + method: POST, + name: "login", + path: "/_matrix/client/r0/login", + rate_limited: true, + requires_authentication: false, + } + + request { + /// Identification information for the user. + #[serde(flatten)] + pub user: UserInfo, + + /// The authentication mechanism. + #[serde(flatten)] + pub login_info: LoginInfo, + + /// ID of the client device + #[serde(skip_serializing_if = "Option::is_none")] + pub device_id: Option, + + /// A display name to assign to the newly-created device. Ignored if device_id corresponds + /// to a known device. + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_device_display_name: Option, + } + + response { + /// The fully-qualified Matrix ID that has been registered. + pub user_id: UserId, + + /// An access token for the account. + pub access_token: String, + + /// The hostname of the homeserver on which the account has been registered. + /// + /// Deprecated: Clients should extract the server_name from user_id (by splitting at the + /// first colon) if they require it. + #[serde(skip_serializing_if = "Option::is_none")] + pub home_server: Option, + + /// ID of the logged-in device. + /// + /// Will be the same as the corresponging parameter in the request, if one was + /// specified. + pub device_id: String, + + /// Client configuration provided by the server. + /// + /// If present, clients SHOULD use the provided object to reconfigure themselves. + pub well_known: Option, + } + + error: crate::Error +} + +/// Identification information for the user. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(from = "user_serde::UserInfo", into = "user_serde::UserInfo")] +pub enum UserInfo { + /// Either a fully qualified Matrix user ID, or just the localpart (as part of the 'identifier' + /// field). + MatrixId(String), + + /// Third party identifier (as part of the 'identifier' field). + ThirdPartyId { + /// Third party identifier for the user. + address: String, + + /// The medium of the identifier. + medium: Medium, + }, + + /// Same as third-party identification with medium == msisdn, but with a non-canonicalised + /// phone number. + PhoneNumber { + /// The country that the phone number is from. + country: String, + + /// The phone number. + phone: String, + }, +} + +/// The authentication mechanism. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum LoginInfo { + /// A password is supplied to authenticate. + #[serde(rename = "m.login.password")] + Password { + /// The password. + password: String, + }, + + /// Token-based login. + #[serde(rename = "m.login.token")] + Token { + /// The token. + token: String, + }, +} + +/// Client configuration provided by the server. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DiscoveryInfo { + /// Information about the homeserver to connect to. + #[serde(rename = "m.homeserver")] + pub homeserver: HomeserverInfo, + + /// Information about the identity server to connect to. + #[serde(rename = "m.identity_server")] + pub identity_server: Option, +} + +/// Information about the homeserver to connect to. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HomeserverInfo { + /// The base URL for the homeserver for client-server connections. + pub base_url: String, +} + +/// Information about the identity server to connect to. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct IdentityServerInfo { + /// The base URL for the identity server for client-server connections. + pub base_url: String, +} + +mod user_serde; + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use serde_json::{from_value as from_json_value, json, Value as JsonValue}; + + use super::{LoginInfo, Medium, Request, UserInfo}; + + #[test] + fn deserialize_login_type() { + assert_eq!( + from_json_value::(json!({ + "type": "m.login.password", + "password": "ilovebananas" + }),) + .unwrap(), + LoginInfo::Password { + password: "ilovebananas".into() + } + ); + + assert_eq!( + from_json_value::(json!({ + "type": "m.login.token", + "token": "1234567890abcdef" + }),) + .unwrap(), + LoginInfo::Token { + token: "1234567890abcdef".into() + } + ); + } + + #[test] + fn deserialize_user() { + assert_eq!( + from_json_value::(json!({ + "identifier": { + "type": "m.id.user", + "user": "cheeky_monkey" + } + })) + .unwrap(), + UserInfo::MatrixId("cheeky_monkey".into()) + ); + } + + #[test] + fn serialize_login_request_body() { + let req: http::Request> = Request { + user: UserInfo::ThirdPartyId { + address: "hello@example.com".to_owned(), + medium: Medium::Email, + }, + login_info: LoginInfo::Token { + token: "0xdeadbeef".to_owned(), + }, + device_id: None, + initial_device_display_name: Some("test".to_string()), + } + .try_into() + .unwrap(); + + let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap(); + assert_eq!( + req_body_value, + json!({ + "identifier": { + "type": "m.id.thirdparty", + "medium": "email", + "address": "hello@example.com" + }, + "type": "m.login.token", + "token": "0xdeadbeef", + "initial_device_display_name": "test", + }) + ) + } +} diff --git a/ruma-client-api/src/r0/session/login/user_serde.rs b/ruma-client-api/src/r0/session/login/user_serde.rs new file mode 100644 index 00000000..9673f8d1 --- /dev/null +++ b/ruma-client-api/src/r0/session/login/user_serde.rs @@ -0,0 +1,54 @@ +//! Helper module for the Serialize / Deserialize impl's for the User struct +//! in the parent module. + +use serde::{Deserialize, Serialize}; + +use super::Medium; + +// The following three structs could just be used in place of the one in the parent module, but +// that one is arguably much easier to deal with. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) struct UserInfo { + pub identifier: UserIdentifier, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "type")] +pub(crate) enum UserIdentifier { + #[serde(rename = "m.id.user")] + MatrixId { user: String }, + #[serde(rename = "m.id.thirdparty")] + ThirdPartyId { medium: Medium, address: String }, + #[serde(rename = "m.id.phone")] + PhoneNumber { country: String, phone: String }, +} + +impl From for UserInfo { + fn from(info: super::UserInfo) -> Self { + use super::UserInfo::*; + + match info { + MatrixId(user) => UserInfo { + identifier: UserIdentifier::MatrixId { user }, + }, + ThirdPartyId { address, medium } => UserInfo { + identifier: UserIdentifier::ThirdPartyId { address, medium }, + }, + PhoneNumber { country, phone } => UserInfo { + identifier: UserIdentifier::PhoneNumber { country, phone }, + }, + } + } +} + +impl From for super::UserInfo { + fn from(info: UserInfo) -> super::UserInfo { + use super::UserInfo::*; + + match info.identifier { + UserIdentifier::MatrixId { user } => MatrixId(user), + UserIdentifier::ThirdPartyId { address, medium } => ThirdPartyId { address, medium }, + UserIdentifier::PhoneNumber { country, phone } => PhoneNumber { country, phone }, + } + } +} diff --git a/ruma-client-api/src/r0/session/logout.rs b/ruma-client-api/src/r0/session/logout.rs new file mode 100644 index 00000000..f3bd75e4 --- /dev/null +++ b/ruma-client-api/src/r0/session/logout.rs @@ -0,0 +1,20 @@ +//! [POST /_matrix/client/r0/logout](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-logout) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Log out of the homeserver.", + method: POST, + name: "logout", + path: "/_matrix/client/r0/logout", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/session/logout_all.rs b/ruma-client-api/src/r0/session/logout_all.rs new file mode 100644 index 00000000..afc20fe1 --- /dev/null +++ b/ruma-client-api/src/r0/session/logout_all.rs @@ -0,0 +1,20 @@ +//! [POST /_matrix/client/r0/logout/all](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-logout-all) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Invalidates all access tokens for a user, so that they can no longer be used for authorization.", + method: POST, + name: "logout_all", + path: "/_matrix/client/r0/logout/all", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/session/sso_login.rs b/ruma-client-api/src/r0/session/sso_login.rs new file mode 100644 index 00000000..b043698d --- /dev/null +++ b/ruma-client-api/src/r0/session/sso_login.rs @@ -0,0 +1,30 @@ +//! [GET /_matrix/client/r0/login/sso/redirect](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-login-sso-redirect) + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "", + method: GET, + name: "sso_login", + path: "/_matrix/client/r0/login/sso/redirect", + rate_limited: false, + requires_authentication: false, + + } + + request { + /// URL to which the homeserver should return the user after completing + /// authentication with the SSO identity provider. + #[ruma_api(query)] + pub redirect_url: String, + } + + response { + /// Redirect URL to the SSO identity provider. + #[ruma_api(header = LOCATION)] + pub location: String, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/state.rs b/ruma-client-api/src/r0/state.rs new file mode 100644 index 00000000..36c8f2b4 --- /dev/null +++ b/ruma-client-api/src/r0/state.rs @@ -0,0 +1,7 @@ +//! Endpoints for managing room state + +pub mod create_state_event_for_empty_key; +pub mod create_state_event_for_key; +pub mod get_state_events; +pub mod get_state_events_for_empty_key; +pub mod get_state_events_for_key; diff --git a/ruma-client-api/src/r0/state/create_state_event_for_empty_key.rs b/ruma-client-api/src/r0/state/create_state_event_for_empty_key.rs new file mode 100644 index 00000000..1fe4083a --- /dev/null +++ b/ruma-client-api/src/r0/state/create_state_event_for_empty_key.rs @@ -0,0 +1,42 @@ +//! [PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-rooms-roomid-state-eventtype) + +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::{EventId, RoomId}; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Send a state event to a room associated with the empty state key.", + method: PUT, + name: "create_state_event_for_empty_key", + path: "/_matrix/client/r0/rooms/:room_id/state/:event_type", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to set the state in. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The type of event to send. + #[ruma_api(path)] + pub event_type: EventType, + + /// The event's content. The type for this field will be updated in a + /// future release, until then you can create a value using + /// `serde_json::value::to_raw_value`. + #[ruma_api(body)] + pub data: Box, + } + + response { + /// A unique identifier for the event. + // This is not declared required in r0.6.0, but that was a bug that has now been fixed: + // https://github.com/matrix-org/matrix-doc/pull/2525 + pub event_id: EventId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/state/create_state_event_for_key.rs b/ruma-client-api/src/r0/state/create_state_event_for_key.rs new file mode 100644 index 00000000..6f546666 --- /dev/null +++ b/ruma-client-api/src/r0/state/create_state_event_for_key.rs @@ -0,0 +1,46 @@ +//! [PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey) + +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::{EventId, RoomId}; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Send a state event to a room associated with a given state key.", + method: PUT, + name: "create_state_event_for_key", + path: "/_matrix/client/r0/rooms/:room_id/state/:event_type/:state_key", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to set the state in. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The type of event to send. + #[ruma_api(path)] + pub event_type: EventType, + + /// The state_key for the state to send. Defaults to the empty string. + #[ruma_api(path)] + pub state_key: String, + + /// The event's content. The type for this field will be updated in a + /// future release, until then you can create a value using + /// `serde_json::value::to_raw_value`. + #[ruma_api(body)] + pub data: Box, + } + + response { + /// A unique identifier for the event. + // This is not declared required in r0.6.0, but that was a bug that has now been fixed: + // https://github.com/matrix-org/matrix-doc/pull/2525 + pub event_id: EventId, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/state/get_state_events.rs b/ruma-client-api/src/r0/state/get_state_events.rs new file mode 100644 index 00000000..6947b717 --- /dev/null +++ b/ruma-client-api/src/r0/state/get_state_events.rs @@ -0,0 +1,32 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/state](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-state) + +use ruma_api::ruma_api; +use ruma_events::{collections::all::StateEvent, EventJson}; +use ruma_identifiers::RoomId; + +ruma_api! { + metadata { + description: "Get state events for a room.", + method: GET, + name: "get_state_events", + path: "/_matrix/client/r0/rooms/:room_id/state", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to look up the state for. + #[ruma_api(path)] + pub room_id: RoomId, + } + + response { + /// If the user is a member of the room this will be the current state of the room as a + /// list of events. If the user has left the room then this will be the state of the + /// room when they left as a list of events. + #[ruma_api(body)] + pub room_state: Vec>, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/state/get_state_events_for_empty_key.rs b/ruma-client-api/src/r0/state/get_state_events_for_empty_key.rs new file mode 100644 index 00000000..c9400be0 --- /dev/null +++ b/ruma-client-api/src/r0/state/get_state_events_for_empty_key.rs @@ -0,0 +1,37 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-state-eventtype) + +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::RoomId; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Get state events of a given type associated with the empty key.", + method: GET, + name: "get_state_events_for_empty_key", + path: "/_matrix/client/r0/rooms/:room_id/state/:event_type", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to look up the state for. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The type of state to look up. + #[ruma_api(path)] + pub event_type: EventType, + } + + response { + /// The content of the state event. + /// + /// To create a `Box`, use `serde_json::value::to_raw_value`. + #[ruma_api(body)] + pub content: Box, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/state/get_state_events_for_key.rs b/ruma-client-api/src/r0/state/get_state_events_for_key.rs new file mode 100644 index 00000000..ad30024a --- /dev/null +++ b/ruma-client-api/src/r0/state/get_state_events_for_key.rs @@ -0,0 +1,39 @@ +//! [GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-state-eventtype-state-key) + +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::RoomId; +use serde_json::value::RawValue as RawJsonValue; + +ruma_api! { + metadata { + description: "Get state events associated with a given key.", + method: GET, + name: "get_state_events_for_key", + path: "/_matrix/client/r0/rooms/:room_id/state/:event_type/:state_key", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room to look up the state for. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The type of state to look up. + #[ruma_api(path)] + pub event_type: EventType, + + /// The key of the state to look up. + #[ruma_api(path)] + pub state_key: String, + } + + response { + /// The content of the state event. + #[ruma_api(body)] + pub content: Box, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/sync.rs b/ruma-client-api/src/r0/sync.rs new file mode 100644 index 00000000..25045f8d --- /dev/null +++ b/ruma-client-api/src/r0/sync.rs @@ -0,0 +1,3 @@ +//! Endpoints for getting and synchronizing events. + +pub mod sync_events; diff --git a/ruma-client-api/src/r0/sync/sync_events.rs b/ruma-client-api/src/r0/sync/sync_events.rs new file mode 100644 index 00000000..dad8a069 --- /dev/null +++ b/ruma-client-api/src/r0/sync/sync_events.rs @@ -0,0 +1,436 @@ +//! [GET /_matrix/client/r0/sync](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-sync) + +use std::{collections::BTreeMap, time::Duration}; + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{ + collections::{ + all::{RoomEvent, StateEvent}, + only::Event as NonRoomEvent, + }, + presence::PresenceEvent, + stripped::AnyStrippedStateEvent, + to_device::AnyToDeviceEvent, + EventJson, +}; +use ruma_identifiers::{RoomId, UserId}; +use serde::{Deserialize, Serialize}; + +use crate::r0::{filter::FilterDefinition, keys::KeyAlgorithm}; + +ruma_api! { + metadata { + description: "Get all new events from all rooms since the last sync or a given point of time.", + method: GET, + name: "sync", + path: "/_matrix/client/r0/sync", + rate_limited: false, + requires_authentication: true, + } + + request { + /// A filter represented either as its full JSON definition or the ID of a saved filter. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub filter: Option, + + /// A point in time to continue a sync from. + /// + /// Should be a token from the `next_batch` field of a previous `/sync` + /// request. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub since: Option, + + /// Controls whether to include the full state for all rooms the user is a member of. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + #[ruma_api(query)] + pub full_state: bool, + + /// Controls whether the client is automatically marked as online by polling this API. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + #[ruma_api(query)] + pub set_presence: PresenceState, + + /// The maximum time to poll in milliseconds before returning this request. + #[serde( + with = "ruma_serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + #[ruma_api(query)] + pub timeout: Option, + } + + response { + /// The batch token to supply in the `since` param of the next `/sync` request. + pub next_batch: String, + + /// Updates to rooms. + #[serde(default, skip_serializing_if = "Rooms::is_empty")] + pub rooms: Rooms, + + /// Updates to the presence status of other users. + #[serde(default, skip_serializing_if = "Presence::is_empty")] + pub presence: Presence, + + /// The global private data created by this user. + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + /// Messages sent dirrectly between devices. + #[serde(default, skip_serializing_if = "ToDevice::is_empty")] + pub to_device: ToDevice, + + /// Information on E2E device updates. + /// + /// Only present on an incremental sync. + #[serde(skip_serializing_if = "Option::is_none")] + pub device_lists: Option, + + /// For each key algorithm, the number of unclaimed one-time keys + /// currently held on the server for a device. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub device_one_time_keys_count: BTreeMap, + } + + error: crate::Error +} + +pub use ruma_common::presence::PresenceState; + +#[deprecated = "use `PresenceState` instead"] +pub use self::PresenceState as SetPresence; + +/// A filter represented either as its full JSON definition or the ID of a saved filter. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[allow(clippy::large_enum_variant)] +#[serde(untagged)] +pub enum Filter { + // The filter definition needs to be (de)serialized twice because it is a URL-encoded JSON + // string. Since #[ruma_api(query)] only does the latter and this is a very uncommon + // setup, we implement it through custom serde logic for this specific enum variant rather than + // adding another ruma_api attribute. + // + // On the deserialization side, because this is an enum with #[serde(untagged)], serde will + // try the variants in order (https://serde.rs/enum-representations.html). That means because + // FilterDefinition is the first variant, JSON decoding is attempted first which is almost + // functionally equivalent to looking at whether the first symbol is a '{' as the spec says. + // (there are probably some corner cases like leading whitespace) + #[serde(with = "ruma_serde::json_string")] + /// A complete filter definition serialized to JSON. + FilterDefinition(FilterDefinition), + + /// The ID of a filter saved on the server. + FilterId(String), +} + +/// Updates to rooms. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Rooms { + /// The rooms that the user has left or been banned from. + pub leave: BTreeMap, + + /// The rooms that the user has joined. + pub join: BTreeMap, + + /// The rooms that the user has been invited to. + pub invite: BTreeMap, +} + +impl Rooms { + fn is_empty(&self) -> bool { + self.leave.is_empty() && self.join.is_empty() && self.invite.is_empty() + } +} + +/// Historical updates to left rooms. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LeftRoom { + /// The timeline of messages and state changes in the room up to the point when the user + /// left. + pub timeline: Timeline, + + /// The state updates for the room up to the start of the timeline. + pub state: State, + + /// The private data that this user has attached to this room. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_data: Option, +} + +/// Updates to joined rooms. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct JoinedRoom { + /// Information about the room which clients may need to correctly render it + /// to users. + pub summary: RoomSummary, + + /// Counts of unread notifications for this room. + pub unread_notifications: UnreadNotificationsCount, + + /// The timeline of messages and state changes in the room. + pub timeline: Timeline, + + /// Updates to the state, between the time indicated by the `since` parameter, and the start + /// of the `timeline` (or all state up to the start of the `timeline`, if `since` is not + /// given, or `full_state` is true). + pub state: State, + + /// The private data that this user has attached to this room. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_data: Option, + + /// The ephemeral events in the room that aren't recorded in the timeline or state of the + /// room. e.g. typing. + pub ephemeral: Ephemeral, +} + +/// unread notifications count +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct UnreadNotificationsCount { + /// The number of unread notifications for this room with the highlight flag set. + #[serde(skip_serializing_if = "Option::is_none")] + pub highlight_count: Option, + + /// The total number of unread notifications for this room. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification_count: Option, +} + +/// Events in the room. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Timeline { + /// True if the number of events returned was limited by the `limit` on the filter. + #[serde(skip_serializing_if = "Option::is_none")] + pub limited: Option, + + /// A token that can be supplied to to the `from` parameter of the + /// `/rooms/{roomId}/messages` endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + + /// A list of events. + pub events: Vec>, +} + +/// State events in the room. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct State { + /// A list of state events. + pub events: Vec>, +} + +/// The private data that this user has attached to this room. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct AccountData { + /// A list of events. + pub events: Vec>, +} + +impl AccountData { + fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Ephemeral events not recorded in the timeline or state of the room. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Ephemeral { + /// A list of events. + pub events: Vec>, +} + +/// Information about room for rendering to clients. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomSummary { + /// Users which can be used to generate a room name if the room does not have + /// one. Required if room name or canonical aliases are not set or empty. + #[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")] + pub heroes: Vec, + + /// Number of users whose membership status is `join`. + /// Required if field has changed since last sync; otherwise, it may be + /// omitted. + #[serde( + rename = "m.joined_member_count", + skip_serializing_if = "Option::is_none" + )] + pub joined_member_count: Option, + + /// Number of users whose membership status is `invite`. + /// Required if field has changed since last sync; otherwise, it may be + /// omitted. + #[serde( + rename = "m.invited_member_count", + skip_serializing_if = "Option::is_none" + )] + pub invited_member_count: Option, +} + +/// Updates to the rooms that the user has been invited to. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InvitedRoom { + /// The state of a room that the user has been invited to. + pub invite_state: InviteState, +} + +/// The state of a room that the user has been invited to. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InviteState { + /// A list of state events. + pub events: Vec>, +} + +/// Updates to the presence status of other users. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Presence { + /// A list of events. + pub events: Vec>, +} + +impl Presence { + fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Messages sent dirrectly between devices. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct ToDevice { + /// A list of to-device events. + pub events: Vec>, +} + +impl ToDevice { + fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Information on E2E device udpates. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DeviceLists { + /// List of users who have updated their device identity keys or who now + /// share an encrypted room with the client since the previous sync + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub changed: Vec, + + /// List of users who no longer share encrypted rooms since the previous sync + /// response. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub left: Vec, +} + +#[cfg(test)] +mod tests { + use std::{convert::TryInto, time::Duration}; + + use matches::assert_matches; + + use super::{Filter, PresenceState, Request}; + + #[test] + fn serialize_all_params() { + let req: http::Request> = Request { + filter: Some(Filter::FilterId("66696p746572".into())), + since: Some("s72594_4483_1934".into()), + full_state: true, + set_presence: PresenceState::Offline, + timeout: Some(Duration::from_millis(30000)), + } + .try_into() + .unwrap(); + + let uri = req.uri(); + let query = uri.query().unwrap(); + + assert_eq!(uri.path(), "/_matrix/client/r0/sync"); + assert!(query.contains("filter=66696p746572")); + assert!(query.contains("since=s72594_4483_1934")); + assert!(query.contains("full_state=true")); + assert!(query.contains("set_presence=offline")); + assert!(query.contains("timeout=30000")) + } + + #[test] + fn deserialize_all_query_params() { + let uri = http::Uri::builder() + .scheme("https") + .authority("matrix.org") + .path_and_query( + "/_matrix/client/r0/sync\ + ?filter=myfilter\ + &since=myts\ + &full_state=false\ + &set_presence=offline\ + &timeout=5000", + ) + .build() + .unwrap(); + + let req: Request = http::Request::builder() + .uri(uri) + .body(Vec::::new()) + .unwrap() + .try_into() + .unwrap(); + + assert_matches!(req.filter, Some(Filter::FilterId(id)) if id == "myfilter"); + assert_eq!(req.since, Some("myts".into())); + assert_eq!(req.full_state, false); + assert_eq!(req.set_presence, PresenceState::Offline); + assert_eq!(req.timeout, Some(Duration::from_millis(5000))); + } + + #[test] + fn deserialize_no_query_params() { + let uri = http::Uri::builder() + .scheme("https") + .authority("matrix.org") + .path_and_query("/_matrix/client/r0/sync") + .build() + .unwrap(); + + let req: Request = http::Request::builder() + .uri(uri) + .body(Vec::::new()) + .unwrap() + .try_into() + .unwrap(); + + assert_matches!(req.filter, None); + assert_eq!(req.since, None); + assert_eq!(req.full_state, false); + assert_eq!(req.set_presence, PresenceState::Online); + assert_eq!(req.timeout, None); + } + + #[test] + fn deserialize_some_query_params() { + let uri = http::Uri::builder() + .scheme("https") + .authority("matrix.org") + .path_and_query( + "/_matrix/client/r0/sync\ + ?filter=EOKFFmdZYF\ + &timeout=0", + ) + .build() + .unwrap(); + + let req: Request = http::Request::builder() + .uri(uri) + .body(Vec::::new()) + .unwrap() + .try_into() + .unwrap(); + + assert_matches!(req.filter, Some(Filter::FilterId(id)) if id == "EOKFFmdZYF"); + assert_eq!(req.since, None); + assert_eq!(req.full_state, false); + assert_eq!(req.set_presence, PresenceState::Online); + assert_eq!(req.timeout, Some(Duration::from_millis(0))); + } +} diff --git a/ruma-client-api/src/r0/tag.rs b/ruma-client-api/src/r0/tag.rs new file mode 100644 index 00000000..7f0d9d2e --- /dev/null +++ b/ruma-client-api/src/r0/tag.rs @@ -0,0 +1,5 @@ +//! Endpoints for tagging rooms. + +pub mod create_tag; +pub mod delete_tag; +pub mod get_tags; diff --git a/ruma-client-api/src/r0/tag/create_tag.rs b/ruma-client-api/src/r0/tag/create_tag.rs new file mode 100644 index 00000000..36e63af9 --- /dev/null +++ b/ruma-client-api/src/r0/tag/create_tag.rs @@ -0,0 +1,38 @@ +//! [PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-user-userid-rooms-roomid-tags-tag) + +use ruma_api::ruma_api; +use ruma_events::tag::TagInfo; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Add a new tag to a room.", + method: PUT, + name: "create_tag", + path: "/_matrix/client/r0/user/:user_id/rooms/:room_id/tags/:tag", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The ID of the user creating the tag. + #[ruma_api(path)] + pub user_id: UserId, + + /// The room to tag. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The name of the tag to create. + #[ruma_api(path)] + pub tag: String, + + /// Info about the tag. + #[ruma_api(body)] + pub tag_info: TagInfo, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/tag/delete_tag.rs b/ruma-client-api/src/r0/tag/delete_tag.rs new file mode 100644 index 00000000..5d086ddc --- /dev/null +++ b/ruma-client-api/src/r0/tag/delete_tag.rs @@ -0,0 +1,33 @@ +//! [DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}](https://matrix.org/docs/spec/client_server/r0.6.0#delete-matrix-client-r0-user-userid-rooms-roomid-tags-tag) + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Remove a tag from a room.", + method: DELETE, + name: "delete_tag", + path: "/_matrix/client/r0/user/:user_id/rooms/:room_id/tags/:tag", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The user whose tag will be deleted. + #[ruma_api(path)] + pub user_id: UserId, + + /// The tagged room. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The name of the tag to delete. + #[ruma_api(path)] + pub tag: String, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/tag/get_tags.rs b/ruma-client-api/src/r0/tag/get_tags.rs new file mode 100644 index 00000000..e4e0d40d --- /dev/null +++ b/ruma-client-api/src/r0/tag/get_tags.rs @@ -0,0 +1,33 @@ +//! [GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-user-userid-rooms-roomid-tags) + +use ruma_api::ruma_api; +use ruma_events::{tag::TagEventContent, EventJson}; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + description: "Get the tags associated with a room.", + method: GET, + name: "get_tags", + path: "/_matrix/client/r0/user/:user_id/rooms/:room_id/tags", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The user whose tags will be retrieved. + #[ruma_api(path)] + pub user_id: UserId, + + /// The room from which tags will be retrieved. + #[ruma_api(path)] + pub room_id: RoomId, + } + + response { + /// The user's tags for the room. + pub tags: EventJson, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/thirdparty.rs b/ruma-client-api/src/r0/thirdparty.rs new file mode 100644 index 00000000..91f890e2 --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty.rs @@ -0,0 +1,97 @@ +//! Endpoints for third party lookups + +pub mod get_location_for_protocol; +pub mod get_location_for_room_alias; +pub mod get_protocol; +pub mod get_protocols; +pub mod get_user_for_protocol; +pub mod get_user_for_user_id; + +use std::collections::BTreeMap; + +use ruma_identifiers::{RoomAliasId, UserId}; + +use serde::{Deserialize, Serialize}; + +/// Metadata about a third party protocol. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Protocol { + /// Fields which may be used to identify a third party user. + pub user_fields: Vec, + + /// Fields which may be used to identify a third party location. + pub location_fields: Vec, + + /// A content URI representing an icon for the third party protocol. + pub icon: String, + + /// The type definitions for the fields defined in `user_fields` and `location_fields`. + pub field_types: BTreeMap, + + /// A list of objects representing independent instances of configuration. + pub instances: Vec, +} + +/// Metadata about an instance of a third party protocol. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ProtocolInstance { + /// A human-readable description for the protocol, such as the name. + pub desc: String, + + /// An optional content URI representing the protocol. + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + + /// Preset values for `fields` the client may use to search by. + pub fields: BTreeMap, + + /// A unique identifier across all instances. + pub network_id: String, +} + +/// A type definition for a field used to identify third party users or locations. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct FieldType { + /// A regular expression for validation of a field's value. + pub regexp: String, + + /// A placeholder serving as a valid example of the field value. + pub placeholder: String, +} + +/// A third party network location. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Location { + /// An alias for a matrix room. + pub alias: RoomAliasId, + + /// The protocol ID that the third party location is a part of. + pub protocol: String, + + /// Information used to identify this third party location. + pub fields: BTreeMap, +} + +/// A third party network user. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct User { + /// A matrix user ID representing a third party user. + pub userid: UserId, + + /// The protocol ID that the third party user is a part of. + pub protocol: String, + + /// Information used to identify this third party user. + pub fields: BTreeMap, +} + +/// The medium of a third party identifier. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Medium { + /// Email address identifier + Email, + + /// Phone number identifier + MSISDN, +} diff --git a/ruma-client-api/src/r0/thirdparty/get_location_for_protocol.rs b/ruma-client-api/src/r0/thirdparty/get_location_for_protocol.rs new file mode 100644 index 00000000..d3aab76d --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty/get_location_for_protocol.rs @@ -0,0 +1,37 @@ +//! [GET /_matrix/client/r0/thirdparty/location/{protocol}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-thirdparty-location-protocol) + +use std::collections::BTreeMap; + +use ruma_api::ruma_api; + +use super::Location; + +ruma_api! { + metadata { + description: "Fetches third party locations for a protocol.", + method: GET, + name: "get_location_for_protocol", + path: "/_matrix/client/r0/thirdparty/location/:protocol", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The protocol used to communicate to the third party network. + #[ruma_api(path)] + pub protocol: String, + + /// One or more custom fields to help identify the third party location. + // The specification is incorrect for this parameter. See matrix-org/matrix-doc#2352. + #[ruma_api(query_map)] + pub fields: BTreeMap, + } + + response { + /// List of matched third party locations. + #[ruma_api(body)] + pub locations: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/thirdparty/get_location_for_room_alias.rs b/ruma-client-api/src/r0/thirdparty/get_location_for_room_alias.rs new file mode 100644 index 00000000..b1038afc --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty/get_location_for_room_alias.rs @@ -0,0 +1,31 @@ +//! [GET /_matrix/client/r0/thirdparty/location](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-thirdparty-location) + +use ruma_api::ruma_api; +use ruma_identifiers::RoomAliasId; + +use super::Location; + +ruma_api! { + metadata { + description: "Retrieve an array of third party network locations from a Matrix room alias.", + method: GET, + name: "get_location_for_room_alias", + path: "/_matrix/client/r0/thirdparty/location", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The Matrix room alias to look up. + #[ruma_api(query)] + pub alias: RoomAliasId, + } + + response { + /// List of matched third party locations. + #[ruma_api(body)] + pub locations: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/thirdparty/get_protocol.rs b/ruma-client-api/src/r0/thirdparty/get_protocol.rs new file mode 100644 index 00000000..61cf67fb --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty/get_protocol.rs @@ -0,0 +1,30 @@ +//! [GET /_matrix/client/r0/thirdparty/protocol/{protocol}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-thirdparty-protocol-protocol) + +use ruma_api::ruma_api; + +use super::Protocol; + +ruma_api! { + metadata { + description: "Fetches the metadata from the homeserver about a particular third party protocol.", + method: GET, + name: "get_protocol", + path: "/_matrix/client/r0/thirdparty/protocol/:protocol", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The name of the protocol. + #[ruma_api(path)] + pub protocol: String, + } + + response { + /// Metadata about the protocol. + #[ruma_api(body)] + pub protocol: Protocol, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/thirdparty/get_protocols.rs b/ruma-client-api/src/r0/thirdparty/get_protocols.rs new file mode 100644 index 00000000..aad510f1 --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty/get_protocols.rs @@ -0,0 +1,28 @@ +//! [GET /_matrix/client/r0/thirdparty/protocols](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-thirdparty-protocols) + +use std::collections::BTreeMap; + +use ruma_api::ruma_api; + +use super::Protocol; + +ruma_api! { + metadata { + description: "Fetches the overall metadata about protocols supported by the homeserver.", + method: GET, + name: "get_protocols", + path: "/_matrix/client/r0/thirdparty/protocols", + rate_limited: false, + requires_authentication: true, + } + + request {} + + response { + /// Metadata about protocols supported by the homeserver. + #[ruma_api(body)] + pub protocols: BTreeMap, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/thirdparty/get_user_for_protocol.rs b/ruma-client-api/src/r0/thirdparty/get_user_for_protocol.rs new file mode 100644 index 00000000..424c4c82 --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty/get_user_for_protocol.rs @@ -0,0 +1,37 @@ +//! [GET /_matrix/client/r0/thirdparty/user/{protocol}](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-thirdparty-user-protocol) + +use std::collections::BTreeMap; + +use ruma_api::ruma_api; + +use super::User; + +ruma_api! { + metadata { + description: "Fetches third party users for a protocol.", + method: GET, + name: "get_user_for_protocol", + path: "/_matrix/client/r0/thirdparty/user/:protocol", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The protocol used to communicate to the third party network. + #[ruma_api(path)] + pub protocol: String, + + /// One or more custom fields that are passed to the AS to help identify the user. + // The specification is incorrect for this parameter. See matrix-org/matrix-doc#2352. + #[ruma_api(query_map)] + pub fields: BTreeMap, + } + + response { + /// List of matched third party users. + #[ruma_api(body)] + pub users: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/thirdparty/get_user_for_user_id.rs b/ruma-client-api/src/r0/thirdparty/get_user_for_user_id.rs new file mode 100644 index 00000000..476f69c3 --- /dev/null +++ b/ruma-client-api/src/r0/thirdparty/get_user_for_user_id.rs @@ -0,0 +1,31 @@ +//! [GET /_matrix/client/r0/thirdparty/user](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-thirdparty-user) + +use ruma_api::ruma_api; +use ruma_identifiers::UserId; + +use super::User; + +ruma_api! { + metadata { + description: "Retrieve an array of third party users from a Matrix User ID.", + method: GET, + name: "get_user_for_user_id", + path: "/_matrix/client/r0/thirdparty/user", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The Matrix User ID to look up. + #[ruma_api(query)] + pub userid: UserId, + } + + response { + /// List of matched third party users. + #[ruma_api(body)] + pub users: Vec, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/to_device.rs b/ruma-client-api/src/r0/to_device.rs new file mode 100644 index 00000000..f75176d8 --- /dev/null +++ b/ruma-client-api/src/r0/to_device.rs @@ -0,0 +1,73 @@ +//! Endpoints for client devices to exchange information not persisted in room DAG. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +use ruma_identifiers::DeviceId; +use serde::{ + de::{self, Unexpected}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +pub mod send_event_to_device; + +/// Represents one or all of a user's devices. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum DeviceIdOrAllDevices { + /// Represents a device Id for one of a user's devices. + DeviceId(DeviceId), + + /// Represents all devices for a user. + AllDevices, +} + +impl Display for DeviceIdOrAllDevices { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + DeviceIdOrAllDevices::DeviceId(device_id) => write!(f, "{}", device_id.to_string()), + DeviceIdOrAllDevices::AllDevices => write!(f, "*"), + } + } +} + +impl TryFrom<&str> for DeviceIdOrAllDevices { + type Error = &'static str; + fn try_from(device_id_or_all_devices: &str) -> Result { + if device_id_or_all_devices.is_empty() { + Err("Device identifier cannot be empty") + } else if "*" == device_id_or_all_devices { + Ok(DeviceIdOrAllDevices::AllDevices) + } else { + Ok(DeviceIdOrAllDevices::DeviceId( + device_id_or_all_devices.to_string(), + )) + } + } +} + +impl Serialize for DeviceIdOrAllDevices { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::DeviceId(ref device_id) => serializer.serialize_str(&device_id), + Self::AllDevices => serializer.serialize_str("*"), + } + } +} + +impl<'de> Deserialize<'de> for DeviceIdOrAllDevices { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = &String::deserialize(deserializer)?; + + DeviceIdOrAllDevices::try_from(&value[..]).map_err(|_| { + de::Error::invalid_value(Unexpected::Str(&value), &"a valid device identifier or '*'") + }) + } +} diff --git a/ruma-client-api/src/r0/to_device/send_event_to_device.rs b/ruma-client-api/src/r0/to_device/send_event_to_device.rs new file mode 100644 index 00000000..f84a33ab --- /dev/null +++ b/ruma-client-api/src/r0/to_device/send_event_to_device.rs @@ -0,0 +1,43 @@ +//! [PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-sendtodevice-eventtype-txnid) + +use std::collections::BTreeMap; + +use ruma_api::ruma_api; +use ruma_events::EventType; +use ruma_identifiers::UserId; +use serde_json::value::RawValue as RawJsonValue; + +use super::DeviceIdOrAllDevices; + +ruma_api! { + metadata { + description: "Send an event to a device or devices.", + method: PUT, + name: "send_event_to_device", + path: "/_matrix/client/r0/sendToDevice/:event_type/:txn_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// Type of event being sent to each device. + #[ruma_api(path)] + pub event_type: EventType, + + /// A request identifier unique to the access token used to send the request. + #[ruma_api(path)] + pub txn_id: String, + + /// A map of users to devices to a content for a message event to be + /// sent to the user's device. Individual message events can be sent + /// to devices, but all events must be of the same type. + /// The content's type for this field will be updated in a future + /// release, until then you can create a value using + /// `serde_json::value::to_raw_value`. + pub messages: BTreeMap>> + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/typing.rs b/ruma-client-api/src/r0/typing.rs new file mode 100644 index 00000000..446155e3 --- /dev/null +++ b/ruma-client-api/src/r0/typing.rs @@ -0,0 +1,3 @@ +//! Endpoints for typing notifications. + +pub mod create_typing_event; diff --git a/ruma-client-api/src/r0/typing/create_typing_event.rs b/ruma-client-api/src/r0/typing/create_typing_event.rs new file mode 100644 index 00000000..23f245ff --- /dev/null +++ b/ruma-client-api/src/r0/typing/create_typing_event.rs @@ -0,0 +1,44 @@ +//! [PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}](https://matrix.org/docs/spec/client_server/r0.6.0#put-matrix-client-r0-rooms-roomid-typing-userid) + +use std::time::Duration; + +use ruma_api::ruma_api; +use ruma_identifiers::{RoomId, UserId}; + +ruma_api! { + metadata { + method: PUT, + path: "/_matrix/client/r0/rooms/:room_id/typing/:user_id", + name: "create_typing_event", + description: "Send a typing event to a room.", + requires_authentication: true, + rate_limited: true, + } + + request { + /// The user who has started to type. + #[ruma_api(path)] + pub user_id: UserId, + + /// The room in which the user is typing. + #[ruma_api(path)] + pub room_id: RoomId, + + // TODO: Group the following two body fields into an enum + + /// Whether the user is typing or not. If `false`, the `timeout` key can be omitted. + pub typing: bool, + + /// The length of time in milliseconds to mark this user as typing. + #[serde( + with = "ruma_serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + pub timeout: Option, + } + + response {} + + error: crate::Error +} diff --git a/ruma-client-api/src/r0/uiaa.rs b/ruma-client-api/src/r0/uiaa.rs new file mode 100644 index 00000000..73994d17 --- /dev/null +++ b/ruma-client-api/src/r0/uiaa.rs @@ -0,0 +1,408 @@ +//! Module for User-Interactive Authentication API types. + +use std::{ + collections::BTreeMap, + fmt::{self, Display, Formatter}, +}; + +use ruma_api::{error::ResponseDeserializationError, EndpointError}; +use serde::{Deserialize, Serialize}; +use serde_json::{ + from_slice as from_json_slice, to_vec as to_json_vec, value::RawValue as RawJsonValue, + Value as JsonValue, +}; + +use crate::error::{Error as MatrixError, ErrorBody}; + +/// Additional authentication information for the user-interactive authentication API. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum AuthData { + /// Used for sending UIAA authentication requests to the homeserver directly + /// from the client. + DirectRequest { + /// The login type that the client is attempting to complete. + #[serde(rename = "type")] + kind: String, + + /// The value of the session key given by the homeserver. + #[serde(skip_serializing_if = "Option::is_none")] + session: Option, + + /// Parameters submitted for a particular authentication stage. + // FIXME: RawJsonValue doesn't work here, is that a bug? + #[serde(flatten)] + auth_parameters: BTreeMap, + }, + + /// Used by the client to acknowledge that the user has completed a UIAA + /// stage through the fallback method. + FallbackAcknowledgement { + /// The value of the session key given by the homeserver. + session: String, + }, +} + +/// Information about available authentication flows and status for +/// User-Interactive Authenticiation API. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UiaaInfo { + /// List of authentication flows available for this endpoint. + pub flows: Vec, + + /// List of stages in the current flow completed by the client. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub completed: Vec, + + /// Authentication parameters required for the client to complete + /// authentication. + /// + /// To create a `Box`, use `serde_json::value::to_raw_value`. + pub params: Box, + + /// Session key for client to use to complete authentication. + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, + + /// Authentication-related errors for previous request returned by homeserver. + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub auth_error: Option, +} + +/// Description of steps required to authenticate via the User-Interactive +/// Authentication API. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct AuthFlow { + /// Ordered list of stages required to complete authentication. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub stages: Vec, +} + +/// Contains either a User-Interactive Authentication API response body or a +/// Matrix error. +#[derive(Clone, Debug)] +pub enum UiaaResponse { + /// User-Interactive Authentication API response + AuthResponse(UiaaInfo), + + /// Matrix error response + MatrixError(MatrixError), +} + +impl Display for UiaaResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::AuthResponse(_) => write!(f, "User-Interactive Authentication required."), + Self::MatrixError(err) => write!(f, "{}", err), + } + } +} + +impl From for UiaaResponse { + fn from(error: MatrixError) -> Self { + Self::MatrixError(error) + } +} + +impl EndpointError for UiaaResponse { + fn try_from_response( + response: http::Response>, + ) -> Result { + if response.status() == http::StatusCode::UNAUTHORIZED { + if let Ok(authentication_info) = from_json_slice::(response.body()) { + return Ok(UiaaResponse::AuthResponse(authentication_info)); + } + } + + MatrixError::try_from_response(response).map(From::from) + } +} + +impl From for http::Response> { + fn from(uiaa_response: UiaaResponse) -> http::Response> { + match uiaa_response { + UiaaResponse::AuthResponse(authentication_info) => http::Response::builder() + .header(http::header::CONTENT_TYPE, "application/json") + .status(&http::StatusCode::UNAUTHORIZED) + .body(to_json_vec(&authentication_info).unwrap()) + .unwrap(), + UiaaResponse::MatrixError(error) => http::Response::from(error), + } + } +} + +#[cfg(test)] +mod tests { + use maplit::btreemap; + use matches::assert_matches; + use ruma_api::EndpointError; + use serde_json::{ + from_slice as from_json_slice, from_str as from_json_str, from_value as from_json_value, + json, to_value as to_json_value, value::to_raw_value as to_raw_json_value, + Value as JsonValue, + }; + + use super::{AuthData, AuthFlow, UiaaInfo, UiaaResponse}; + use crate::error::{ErrorBody, ErrorKind}; + + #[test] + fn test_serialize_authentication_data_direct_request() { + let authentication_data = AuthData::DirectRequest { + kind: "example.type.foo".to_string(), + session: Some("ZXY000".to_string()), + auth_parameters: btreemap! { + "example_credential".to_owned() => json!("verypoorsharedsecret") + }, + }; + + assert_eq!( + json!({ + "type": "example.type.foo", + "session": "ZXY000", + "example_credential": "verypoorsharedsecret", + }), + to_json_value(authentication_data).unwrap() + ); + } + + #[test] + fn test_deserialize_authentication_data_direct_request() { + let json = json!({ + "type": "example.type.foo", + "session": "opaque_session_id", + "example_credential": "verypoorsharedsecret", + }); + + assert_matches!( + from_json_value::(json).unwrap(), + AuthData::DirectRequest { kind, session: Some(session), auth_parameters } + if kind == "example.type.foo" + && session == "opaque_session_id" + && auth_parameters == btreemap!{ + "example_credential".to_owned() => json!("verypoorsharedsecret") + } + ); + } + + #[test] + fn test_serialize_authentication_data_fallback() { + let authentication_data = AuthData::FallbackAcknowledgement { + session: "ZXY000".to_string(), + }; + + assert_eq!( + json!({ "session": "ZXY000" }), + to_json_value(authentication_data).unwrap() + ); + } + + #[test] + fn test_deserialize_authentication_data_fallback() { + let json = json!({ "session": "opaque_session_id" }); + + assert_matches!( + from_json_value::(json).unwrap(), + AuthData::FallbackAcknowledgement { session } + if session == "opaque_session_id" + ); + } + + #[test] + fn test_serialize_uiaa_info() { + let uiaa_info = UiaaInfo { + flows: vec![AuthFlow { + stages: vec!["m.login.password".to_string(), "m.login.dummy".to_string()], + }], + completed: vec!["m.login.password".to_string()], + params: to_raw_json_value(&json!({ + "example.type.baz": { + "example_key": "foobar" + } + })) + .unwrap(), + session: None, + auth_error: None, + }; + + let json = json!({ + "flows": [{ "stages": ["m.login.password", "m.login.dummy"] }], + "completed": ["m.login.password"], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + } + }); + assert_eq!(to_json_value(uiaa_info).unwrap(), json); + } + + #[test] + fn test_deserialize_uiaa_info() { + let json = json!({ + "errcode": "M_FORBIDDEN", + "error": "Invalid password", + "completed": ["example.type.foo"], + "flows": [ + { + "stages": ["example.type.foo", "example.type.bar"] + }, + { + "stages": ["example.type.foo", "example.type.baz"] + } + ], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + }, + "session": "xxxxxx" + }); + + assert_matches!( + from_json_value::(json).unwrap(), + UiaaInfo { + auth_error: Some(ErrorBody { + kind: ErrorKind::Forbidden, + message: error_message, + }), + completed, + flows, + params, + session: Some(session), + } if error_message == "Invalid password" + && completed == vec!["example.type.foo".to_string()] + && flows == vec![ + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.bar".to_string(), + ], + }, + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.baz".to_string(), + ], + }, + ] + && from_json_str::(params.get()).unwrap() == json!({ + "example.type.baz": { + "example_key": "foobar" + } + }) + && session == "xxxxxx" + ); + } + + #[test] + fn test_try_uiaa_response_into_http_response() { + let uiaa_info = UiaaInfo { + flows: vec![AuthFlow { + stages: vec!["m.login.password".to_string(), "m.login.dummy".to_string()], + }], + completed: vec!["m.login.password".to_string()], + params: to_raw_json_value(&json!({ + "example.type.baz": { + "example_key": "foobar" + } + })) + .unwrap(), + session: None, + auth_error: None, + }; + let uiaa_response: http::Response> = UiaaResponse::AuthResponse(uiaa_info).into(); + + assert_matches!( + from_json_slice::(uiaa_response.body()).unwrap(), + UiaaInfo { + flows, + completed, + params, + session: None, + auth_error: None, + } if flows == vec![AuthFlow { + stages: vec!["m.login.password".to_string(), "m.login.dummy".to_string()], + }] + && completed == vec!["m.login.password".to_string()] + && from_json_str::(params.get()).unwrap() == json!({ + "example.type.baz": { + "example_key": "foobar" + } + }) + ); + assert_eq!( + uiaa_response.status(), + http::status::StatusCode::UNAUTHORIZED + ); + } + + #[test] + fn test_try_uiaa_response_from_http_response() { + let json = serde_json::to_string(&json!({ + "errcode": "M_FORBIDDEN", + "error": "Invalid password", + "completed": [ "example.type.foo" ], + "flows": [ + { + "stages": [ "example.type.foo", "example.type.bar" ] + }, + { + "stages": [ "example.type.foo", "example.type.baz" ] + } + ], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + }, + "session": "xxxxxx" + })) + .unwrap(); + + let http_response = http::Response::builder() + .status(http::StatusCode::UNAUTHORIZED) + .body(json.into()) + .unwrap(); + + let parsed_uiaa_info = match UiaaResponse::try_from_response(http_response).unwrap() { + UiaaResponse::AuthResponse(uiaa_info) => uiaa_info, + _ => panic!("Expected UiaaResponse::AuthResponse"), + }; + + assert_matches!( + parsed_uiaa_info, + UiaaInfo { + auth_error: Some(ErrorBody { + kind: ErrorKind::Forbidden, + message: error_message, + }), + completed, + flows, + params, + session: Some(session), + } if error_message == "Invalid password" + && completed == vec!["example.type.foo".to_string()] + && flows == vec![ + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.bar".to_string(), + ], + }, + AuthFlow { + stages: vec![ + "example.type.foo".to_string(), + "example.type.baz".to_string(), + ], + }, + ] + && from_json_str::(params.get()).unwrap() == json!({ + "example.type.baz": { + "example_key": "foobar" + } + }) + && session == "xxxxxx" + ); + } +} diff --git a/ruma-client-api/src/r0/user_directory.rs b/ruma-client-api/src/r0/user_directory.rs new file mode 100644 index 00000000..f63505ed --- /dev/null +++ b/ruma-client-api/src/r0/user_directory.rs @@ -0,0 +1,3 @@ +//! Endpoints for the user directory. + +pub mod search_users; diff --git a/ruma-client-api/src/r0/user_directory/search_users.rs b/ruma-client-api/src/r0/user_directory/search_users.rs new file mode 100644 index 00000000..96842259 --- /dev/null +++ b/ruma-client-api/src/r0/user_directory/search_users.rs @@ -0,0 +1,53 @@ +//! [POST /_matrix/client/r0/user_directory/search](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-user-directory-search) + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_identifiers::UserId; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Performs a search for users.", + method: POST, + name: "search_users", + path: "/_matrix/client/r0/user_directory/search", + rate_limited: true, + requires_authentication: true, + } + + request { + /// The term to search for. + pub search_term: String, + + /// The maximum number of results to return. + /// + /// Defaults to 10. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + } + + response { + /// Ordered by rank and then whether or not profile info is available. + pub results: Vec, + + /// Indicates if the result list has been truncated by the limit. + pub limited: bool, + } + + error: crate::Error +} + +/// User data as result of a search. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct User { + /// The user's matrix user ID. + pub user_id: UserId, + + /// The display name of the user, if one exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + + /// The avatar url, as an MXC, if one exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, +} diff --git a/ruma-client-api/src/r0/voip.rs b/ruma-client-api/src/r0/voip.rs new file mode 100644 index 00000000..878b59ac --- /dev/null +++ b/ruma-client-api/src/r0/voip.rs @@ -0,0 +1,3 @@ +//! Endpoints for Voice over IP. + +pub mod get_turn_server_info; diff --git a/ruma-client-api/src/r0/voip/get_turn_server_info.rs b/ruma-client-api/src/r0/voip/get_turn_server_info.rs new file mode 100644 index 00000000..dd056ea6 --- /dev/null +++ b/ruma-client-api/src/r0/voip/get_turn_server_info.rs @@ -0,0 +1,35 @@ +//! [GET /_matrix/client/r0/voip/turnServer](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-voip-turnserver) + +use std::time::Duration; + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Get credentials for the client to use when initiating VoIP calls.", + method: GET, + name: "turn_server_info", + path: "_matrix/client/r0/voip/turnServer", + rate_limited: true, + requires_authentication: true, + } + + request {} + + response { + /// The username to use. + pub username: String, + + /// The password to use. + pub password: String, + + /// A list of TURN URIs. + pub uris: Vec, + + /// The time-to-live in seconds. + #[serde(with = "ruma_serde::duration::secs")] + pub ttl: Duration, + } + + error: crate::Error +} diff --git a/ruma-client-api/src/unversioned.rs b/ruma-client-api/src/unversioned.rs new file mode 100644 index 00000000..1622a72d --- /dev/null +++ b/ruma-client-api/src/unversioned.rs @@ -0,0 +1,4 @@ +//! Endpoints that cannot change with new versions of the Matrix specification. + +pub mod discover_homeserver; +pub mod get_supported_versions; diff --git a/ruma-client-api/src/unversioned/discover_homeserver.rs b/ruma-client-api/src/unversioned/discover_homeserver.rs new file mode 100644 index 00000000..b82ef0de --- /dev/null +++ b/ruma-client-api/src/unversioned/discover_homeserver.rs @@ -0,0 +1,43 @@ +//! [GET /.well-known/matrix/client](https://matrix.org/docs/spec/client_server/r0.6.0#get-well-known-matrix-client) + +use ruma_api::ruma_api; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata { + description: "Get discovery information about the domain.", + method: GET, + name: "discover_homeserver", + path: "/.well-known/matrix/client", + rate_limited: false, + requires_authentication: false, + } + + request {} + + response { + /// Information about the homeserver to connect to. + #[serde(rename = "m.homeserver")] + pub homeserver: HomeserverInfo, + + /// Information about the identity server to connect to. + #[serde(rename = "m.identity_server")] + pub identity_server: Option, + } + + error: crate::Error +} + +/// Information about a discovered homeserver. +#[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)] +pub struct HomeserverInfo { + /// The base URL for the homeserver for client-server connections. + pub base_url: String, +} + +/// Information about a discovered identity server. +#[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)] +pub struct IdentityServerInfo { + /// The base URL for the identity server for client-server connections. + pub base_url: String, +} diff --git a/ruma-client-api/src/unversioned/get_supported_versions.rs b/ruma-client-api/src/unversioned/get_supported_versions.rs new file mode 100644 index 00000000..f248c17e --- /dev/null +++ b/ruma-client-api/src/unversioned/get_supported_versions.rs @@ -0,0 +1,29 @@ +//! [GET /_matrix/client/versions](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-versions) + +use std::collections::BTreeMap; + +use ruma_api::ruma_api; + +ruma_api! { + metadata { + description: "Get the versions of the client-server API supported by this homeserver.", + method: GET, + name: "api_versions", + path: "/_matrix/client/versions", + rate_limited: false, + requires_authentication: false, + } + + request {} + + response { + /// A list of Matrix client API protocol versions supported by the homeserver. + pub versions: Vec, + + /// Experimental features supported by the server. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unstable_features: BTreeMap + } + + error: crate::Error +}