Add 'ruma-client/' from commit 'fab0ef566f143c1926bff4af4a0a4751379eff1f'
git-subtree-dir: ruma-client git-subtree-mainline: c4558774be98863015c51e0ec8f26b3fe3d4a103 git-subtree-split: fab0ef566f143c1926bff4af4a0a4751379eff1f
This commit is contained in:
commit
3a6bdd63bd
27
ruma-client/.builds/beta.yml
Normal file
27
ruma-client/.builds/beta.yml
Normal file
@ -0,0 +1,27 @@
|
||||
image: archlinux
|
||||
packages:
|
||||
- rustup
|
||||
sources:
|
||||
- https://github.com/ruma/ruma-client
|
||||
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
|
||||
|
||||
# We don't want the build to stop on individual failure of independent
|
||||
# tools, so capture tool exit codes and set the task exit code manually
|
||||
set +e
|
||||
|
||||
cargo fmt -- --check
|
||||
fmt_exit=$?
|
||||
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
clippy_exit=$?
|
||||
|
||||
cargo test --verbose
|
||||
test_exit=$?
|
||||
|
||||
exit $(( $fmt_exit || $clippy_exit || $test_exit ))
|
32
ruma-client/.builds/nightly.yml
Normal file
32
ruma-client/.builds/nightly.yml
Normal file
@ -0,0 +1,32 @@
|
||||
image: archlinux
|
||||
packages:
|
||||
- rustup
|
||||
sources:
|
||||
- https://github.com/ruma/ruma-client
|
||||
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
|
||||
|
||||
# We don't want the build to stop on individual failure of independent
|
||||
# tools, so capture tool exit codes and set the task exit code manually
|
||||
set +e
|
||||
|
||||
if ( rustup component list | grep -q rustfmt ); then
|
||||
cargo fmt -- --check
|
||||
fi
|
||||
fmt_exit=$?
|
||||
|
||||
if ( rustup component list | grep -q clippy ); then
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
fi
|
||||
clippy_exit=$?
|
||||
|
||||
exit $(( $fmt_exit || $clippy_exit ))
|
29
ruma-client/.builds/stable.yml
Normal file
29
ruma-client/.builds/stable.yml
Normal file
@ -0,0 +1,29 @@
|
||||
image: archlinux
|
||||
packages:
|
||||
- rustup
|
||||
sources:
|
||||
- https://github.com/ruma/ruma-client
|
||||
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
|
||||
|
||||
# We don't want the build to stop on individual failure of independent
|
||||
# tools, so capture tool exit codes and set the task exit code manually
|
||||
set +e
|
||||
|
||||
cargo fmt -- --check
|
||||
fmt_exit=$?
|
||||
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
clippy_exit=$?
|
||||
|
||||
cargo test --verbose
|
||||
test_exit=$?
|
||||
|
||||
exit $(( $fmt_exit || $clippy_exit || $test_exit ))
|
||||
# TODO: Add audit task once cargo-audit binary releases are available.
|
||||
# See https://github.com/RustSec/cargo-audit/issues/66
|
2
ruma-client/.gitignore
vendored
Normal file
2
ruma-client/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/Cargo.lock
|
||||
/target
|
40
ruma-client/Cargo.toml
Normal file
40
ruma-client/Cargo.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[package]
|
||||
authors = [
|
||||
"Jimmy Cuadra <jimmy@jimmycuadra.com>",
|
||||
"Jonas Platte <jplatte@posteo.de>",
|
||||
]
|
||||
categories = ["api-bindings", "web-programming"]
|
||||
description = "A Matrix client library."
|
||||
documentation = "https://docs.rs/ruma-client"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/ruma/ruma-client"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "MIT"
|
||||
name = "ruma-client"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/ruma/ruma-client"
|
||||
version = "0.4.0"
|
||||
|
||||
[dependencies]
|
||||
futures-core = "0.3.5"
|
||||
futures-util = "0.3.5"
|
||||
http = "0.2.1"
|
||||
hyper = "0.13.5"
|
||||
hyper-tls = { version = "0.4.1", optional = true }
|
||||
ruma-api = "=0.17.0-alpha.1"
|
||||
ruma-client-api = "=0.10.0-alpha.1"
|
||||
ruma-common = "0.2.0"
|
||||
ruma-events = "=0.22.0-alpha.1"
|
||||
ruma-identifiers = "0.17.1"
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
serde_json = "1.0.53"
|
||||
serde_urlencoded = "0.6.1"
|
||||
url = "2.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.31"
|
||||
tokio = { version = "0.2.21", features = ["macros"] }
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
tls = ["hyper-tls"]
|
20
ruma-client/LICENSE
Normal file
20
ruma-client/LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
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.
|
||||
|
38
ruma-client/README.md
Normal file
38
ruma-client/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# ruma-client
|
||||
|
||||
[](https://crates.io/crates/ruma-client)
|
||||
[](https://docs.rs/ruma-client/)
|
||||
[](https://travis-ci.org/ruma/ruma-client)
|
||||

|
||||
|
||||
**ruma-client** is a [Matrix][] client library for [Rust][].
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
## Status
|
||||
|
||||
This project is a work in progress and not ready for production usage yet. Most endpoints that are
|
||||
available in this crate are usable with an up-to-date synapse server, but no comprehensive testing
|
||||
has been done so far.
|
||||
|
||||
As long as the matrix client-server API is still at version 0.x, only the latest API revision is
|
||||
considered supported. However, due to the low amount of available manpower, it can take a long time
|
||||
for all changes from a new API revision to arrive in ruma-client (at the time of writing only few
|
||||
endpoints have received an update for r0.4.0).
|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to help out, have a look at the issues here and on the other [ruma-\*][gh-org]
|
||||
repositories (ruma-client-api and ruma-events in particular contain much of the code that powers
|
||||
ruma-client).
|
||||
|
||||
There is also a [room for ruma on matrix.org][#ruma:matrix.org], which can be used for questions
|
||||
and discussion related to any of the crates in this project.
|
||||
|
||||
[gh-org]: https://github.com/ruma
|
||||
[#ruma:matrix.org]: https://matrix.to/#/#ruma:matrix.org
|
||||
|
||||
## Minimum Rust version
|
||||
|
||||
ruma-client requires Rust 1.39.0 or later.
|
65
ruma-client/examples/hello_world.rs
Normal file
65
ruma-client/examples/hello_world.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::{convert::TryFrom, env, process::exit};
|
||||
|
||||
use ruma_client::{
|
||||
self,
|
||||
api::r0,
|
||||
events::{
|
||||
room::message::{MessageEventContent, TextMessageEventContent},
|
||||
EventType,
|
||||
},
|
||||
identifiers::RoomAliasId,
|
||||
Client,
|
||||
};
|
||||
use serde_json::value::to_raw_value as to_raw_json_value;
|
||||
use url::Url;
|
||||
|
||||
async fn hello_world(homeserver_url: Url, room: String) -> anyhow::Result<()> {
|
||||
let client = Client::new(homeserver_url, None);
|
||||
|
||||
client.register_guest().await?;
|
||||
let response = client
|
||||
.request(r0::alias::get_alias::Request {
|
||||
room_alias: RoomAliasId::try_from(&room[..]).unwrap(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let room_id = response.room_id;
|
||||
|
||||
client
|
||||
.request(r0::membership::join_room_by_id::Request {
|
||||
room_id: room_id.clone(),
|
||||
third_party_signed: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
client
|
||||
.request(r0::message::create_message_event::Request {
|
||||
room_id,
|
||||
event_type: EventType::RoomMessage,
|
||||
txn_id: "1".to_owned(),
|
||||
data: to_raw_json_value(&MessageEventContent::Text(TextMessageEventContent {
|
||||
body: "Hello World!".to_owned(),
|
||||
formatted: None,
|
||||
relates_to: None,
|
||||
}))?,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (homeserver_url, room) = match (env::args().nth(1), env::args().nth(2)) {
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <room>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
hello_world(homeserver_url.parse().unwrap(), room).await
|
||||
}
|
81
ruma-client/examples/message_log.rs
Normal file
81
ruma-client/examples/message_log.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::{env, process::exit, time::Duration};
|
||||
|
||||
use futures_util::stream::{StreamExt as _, TryStreamExt as _};
|
||||
use ruma_client::{
|
||||
self,
|
||||
events::room::message::{MessageEventContent, TextMessageEventContent},
|
||||
HttpClient,
|
||||
};
|
||||
use ruma_common::presence::PresenceState;
|
||||
use ruma_events::{AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent};
|
||||
use url::Url;
|
||||
|
||||
async fn log_messages(
|
||||
homeserver_url: Url,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = HttpClient::new(homeserver_url, None);
|
||||
|
||||
client.log_in(username, password, None, None).await?;
|
||||
|
||||
let mut sync_stream = Box::pin(
|
||||
client
|
||||
.sync(
|
||||
None,
|
||||
None,
|
||||
PresenceState::Online,
|
||||
Some(Duration::from_secs(30)),
|
||||
)
|
||||
// TODO: This is a horrible way to obtain an initial next_batch token that generates way
|
||||
// too much server load and network traffic. Fix this!
|
||||
.skip(1),
|
||||
);
|
||||
|
||||
while let Some(res) = sync_stream.try_next().await? {
|
||||
// Only look at rooms the user hasn't left yet
|
||||
for (room_id, room) in res.rooms.join {
|
||||
for event in room
|
||||
.timeline
|
||||
.events
|
||||
.into_iter()
|
||||
.flat_map(|r| r.deserialize())
|
||||
{
|
||||
// Filter out the text messages
|
||||
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(
|
||||
SyncMessageEvent {
|
||||
content:
|
||||
MessageEventContent::Text(TextMessageEventContent {
|
||||
body: msg_body, ..
|
||||
}),
|
||||
sender,
|
||||
..
|
||||
},
|
||||
)) = event
|
||||
{
|
||||
println!("{:?} in {:?}: {}", sender, room_id, msg_body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
let server = Url::parse(&homeserver_url).unwrap();
|
||||
log_messages(server, username, password).await
|
||||
}
|
69
ruma-client/src/error.rs
Normal file
69
ruma-client/src/error.rs
Normal file
@ -0,0 +1,69 @@
|
||||
//! Error conditions.
|
||||
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
use ruma_api::error::{FromHttpResponseError, IntoHttpError};
|
||||
|
||||
/// An error that can occur during client operations.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error<E> {
|
||||
/// Queried endpoint requires authentication but was called on an anonymous client.
|
||||
AuthenticationRequired,
|
||||
/// Construction of the HTTP request failed (this should never happen).
|
||||
IntoHttp(IntoHttpError),
|
||||
/// The request's URL is invalid (this should never happen).
|
||||
Url(UrlError),
|
||||
/// Couldn't obtain an HTTP response (e.g. due to network or DNS issues).
|
||||
Response(ResponseError),
|
||||
/// Converting the HTTP response to one of ruma's types failed.
|
||||
FromHttpResponse(FromHttpResponseError<E>),
|
||||
}
|
||||
|
||||
impl<E: Display> Display for Error<E> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::AuthenticationRequired => {
|
||||
write!(f, "The queried endpoint requires authentication but was called with an anonymous client.")
|
||||
}
|
||||
Self::IntoHttp(err) => write!(f, "HTTP request construction failed: {}", err),
|
||||
Self::Url(UrlError(err)) => write!(f, "Invalid URL: {}", err),
|
||||
Self::Response(ResponseError(err)) => write!(f, "Couldn't obtain a response: {}", err),
|
||||
Self::FromHttpResponse(err) => write!(f, "HTTP response conversion failed: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<IntoHttpError> for Error<E> {
|
||||
fn from(err: IntoHttpError) -> Self {
|
||||
Error::IntoHttp(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl<E> From<http::uri::InvalidUri> for Error<E> {
|
||||
fn from(err: http::uri::InvalidUri) -> Self {
|
||||
Error::Url(UrlError(err))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl<E> From<hyper::Error> for Error<E> {
|
||||
fn from(err: hyper::Error) -> Self {
|
||||
Error::Response(ResponseError(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<FromHttpResponseError<E>> for Error<E> {
|
||||
fn from(err: FromHttpResponseError<E>) -> Self {
|
||||
Error::FromHttpResponse(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Debug + Display> std::error::Error for Error<E> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UrlError(http::uri::InvalidUri);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ResponseError(hyper::Error);
|
423
ruma-client/src/lib.rs
Normal file
423
ruma-client/src/lib.rs
Normal file
@ -0,0 +1,423 @@
|
||||
//! Crate `ruma_client` is a [Matrix](https://matrix.org/) client library.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Begin by creating a `Client` type, usually using the `https` method for a client that supports
|
||||
//! secure connections, and then logging in:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use ruma_client::Client;
|
||||
//!
|
||||
//! let work = async {
|
||||
//! let homeserver_url = "https://example.com".parse().unwrap();
|
||||
//! let client = Client::https(homeserver_url, None);
|
||||
//!
|
||||
//! let session = client
|
||||
//! .log_in("@alice:example.com".to_string(), "secret".to_string(), None, None)
|
||||
//! .await?;
|
||||
//!
|
||||
//! // You're now logged in! Write the session to a file if you want to restore it later.
|
||||
//! // Then start using the API!
|
||||
//! # Result::<(), ruma_client::Error<_>>::Ok(())
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
//! You can also pass an existing session to the `Client` constructor to restore a previous session
|
||||
//! rather than calling `log_in`. This can also be used to create a session for an application service
|
||||
//! that does not need to log in, but uses the access_token directly:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use ruma_client::{Client, Session};
|
||||
//!
|
||||
//! let work = async {
|
||||
//! let homeserver_url = "https://example.com".parse().unwrap();
|
||||
//! let session = Session{access_token: "as_access_token".to_string(), identification: None};
|
||||
//! let client = Client::https(homeserver_url, Some(session));
|
||||
//!
|
||||
//! // make calls to the API
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
//! For the standard use case of synchronizing with the homeserver (i.e. getting all the latest
|
||||
//! events), use the `Client::sync`:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use std::time::Duration;
|
||||
//!
|
||||
//! # use futures_util::stream::{StreamExt as _, TryStreamExt as _};
|
||||
//! # use ruma_client::Client;
|
||||
//! # use ruma_common::presence::PresenceState;
|
||||
//! # let homeserver_url = "https://example.com".parse().unwrap();
|
||||
//! # let client = Client::https(homeserver_url, None);
|
||||
//! # let next_batch_token = String::new();
|
||||
//! # async {
|
||||
//! let mut sync_stream = Box::pin(client.sync(
|
||||
//! None,
|
||||
//! Some(next_batch_token),
|
||||
//! PresenceState::Online,
|
||||
//! Some(Duration::from_secs(30)),
|
||||
//! ));
|
||||
//! while let Some(response) = sync_stream.try_next().await? {
|
||||
//! // Do something with the data in the response...
|
||||
//! }
|
||||
//! # Result::<(), ruma_client::Error<_>>::Ok(())
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! The `Client` type also provides methods for registering a new account if you don't already have
|
||||
//! one with the given homeserver.
|
||||
//!
|
||||
//! Beyond these basic convenience methods, `ruma-client` gives you access to the entire Matrix
|
||||
//! client-server API via the `api` module. Each leaf module under this tree of modules contains
|
||||
//! the necessary types for one API endpoint. Simply call the module's `call` method, passing it
|
||||
//! the logged in `Client` and the relevant `Request` type. `call` will return a future that will
|
||||
//! resolve to the relevant `Response` type.
|
||||
//!
|
||||
//! For example:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use ruma_client::Client;
|
||||
//! # let homeserver_url = "https://example.com".parse().unwrap();
|
||||
//! # let client = Client::https(homeserver_url, None);
|
||||
//! use std::convert::TryFrom;
|
||||
//!
|
||||
//! use ruma_client::api::r0::alias::get_alias;
|
||||
//! use ruma_identifiers::{RoomAliasId, RoomId};
|
||||
//!
|
||||
//! async {
|
||||
//! let response = client
|
||||
//! .request(get_alias::Request {
|
||||
//! room_alias: RoomAliasId::try_from("#example_room:example.com").unwrap(),
|
||||
//! })
|
||||
//! .await?;
|
||||
//!
|
||||
//! assert_eq!(response.room_id, RoomId::try_from("!n8f893n9:example.com").unwrap());
|
||||
//! # Result::<(), ruma_client::Error<_>>::Ok(())
|
||||
//! }
|
||||
//! # ;
|
||||
//! ```
|
||||
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
missing_docs
|
||||
)]
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_core::{
|
||||
future::Future,
|
||||
stream::{Stream, TryStream},
|
||||
};
|
||||
use futures_util::stream;
|
||||
use http::Response as HttpResponse;
|
||||
use hyper::{client::HttpConnector, Client as HyperClient, Uri};
|
||||
#[cfg(feature = "hyper-tls")]
|
||||
use hyper_tls::HttpsConnector;
|
||||
use ruma_api::Endpoint;
|
||||
use ruma_identifiers::DeviceId;
|
||||
use std::collections::BTreeMap;
|
||||
use url::Url;
|
||||
|
||||
pub use ruma_client_api as api;
|
||||
pub use ruma_events as events;
|
||||
pub use ruma_identifiers as identifiers;
|
||||
|
||||
mod error;
|
||||
mod session;
|
||||
|
||||
pub use self::{error::Error, session::Identification, session::Session};
|
||||
|
||||
/// A client for the Matrix client-server API.
|
||||
#[derive(Debug)]
|
||||
pub struct Client<C>(Arc<ClientData<C>>);
|
||||
|
||||
/// Data contained in Client's Rc
|
||||
#[derive(Debug)]
|
||||
struct ClientData<C> {
|
||||
/// The URL of the homeserver to connect to.
|
||||
homeserver_url: Url,
|
||||
/// The underlying HTTP client.
|
||||
hyper: HyperClient<C>,
|
||||
/// User session data.
|
||||
session: Mutex<Option<Session>>,
|
||||
}
|
||||
|
||||
/// Non-secured variant of the client (using plain HTTP requests)
|
||||
pub type HttpClient = Client<HttpConnector>;
|
||||
|
||||
impl HttpClient {
|
||||
/// Creates a new client for making HTTP requests to the given homeserver.
|
||||
pub fn new(homeserver_url: Url, session: Option<Session>) -> Self {
|
||||
Self(Arc::new(ClientData {
|
||||
homeserver_url,
|
||||
hyper: HyperClient::builder().build_http(),
|
||||
session: Mutex::new(session),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Secured variant of the client (using HTTPS requests)
|
||||
#[cfg(feature = "tls")]
|
||||
pub type HttpsClient = Client<HttpsConnector<HttpConnector>>;
|
||||
|
||||
#[cfg(feature = "tls")]
|
||||
impl HttpsClient {
|
||||
/// Creates a new client for making HTTPS requests to the given homeserver.
|
||||
pub fn https(homeserver_url: Url, session: Option<Session>) -> Self {
|
||||
let connector = HttpsConnector::new();
|
||||
|
||||
Self(Arc::new(ClientData {
|
||||
homeserver_url,
|
||||
hyper: HyperClient::builder().build(connector),
|
||||
session: Mutex::new(session),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Client<C>
|
||||
where
|
||||
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
|
||||
{
|
||||
/// Creates a new client using the given `hyper::Client`.
|
||||
///
|
||||
/// This allows the user to configure the details of HTTP as desired.
|
||||
pub fn custom(
|
||||
hyper_client: HyperClient<C>,
|
||||
homeserver_url: Url,
|
||||
session: Option<Session>,
|
||||
) -> Self {
|
||||
Self(Arc::new(ClientData {
|
||||
homeserver_url,
|
||||
hyper: hyper_client,
|
||||
session: Mutex::new(session),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a copy of the current `Session`, if any.
|
||||
///
|
||||
/// Useful for serializing and persisting the session to be restored later.
|
||||
pub fn session(&self) -> Option<Session> {
|
||||
self.0
|
||||
.session
|
||||
.lock()
|
||||
.expect("session mutex was poisoned")
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Log in with a username and password.
|
||||
///
|
||||
/// In contrast to `api::r0::session::login::call()`, this method stores the
|
||||
/// session data returned by the endpoint in this client, instead of
|
||||
/// returning it.
|
||||
pub async fn log_in(
|
||||
&self,
|
||||
user: String,
|
||||
password: String,
|
||||
device_id: Option<Box<DeviceId>>,
|
||||
initial_device_display_name: Option<String>,
|
||||
) -> Result<Session, Error<api::Error>> {
|
||||
use api::r0::session::login;
|
||||
|
||||
let response = self
|
||||
.request(login::Request {
|
||||
user: login::UserInfo::MatrixId(user),
|
||||
login_info: login::LoginInfo::Password { password },
|
||||
device_id,
|
||||
initial_device_display_name,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let session = Session {
|
||||
access_token: response.access_token,
|
||||
identification: Some(Identification {
|
||||
device_id: response.device_id,
|
||||
user_id: response.user_id,
|
||||
}),
|
||||
};
|
||||
*self.0.session.lock().unwrap() = Some(session.clone());
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Register as a guest. In contrast to `api::r0::account::register::call()`,
|
||||
/// this method stores the session data returned by the endpoint in this
|
||||
/// client, instead of returning it.
|
||||
pub async fn register_guest(&self) -> Result<Session, Error<api::r0::uiaa::UiaaResponse>> {
|
||||
use api::r0::account::register;
|
||||
|
||||
let response = self
|
||||
.request(register::Request {
|
||||
auth: None,
|
||||
device_id: None,
|
||||
inhibit_login: false,
|
||||
initial_device_display_name: None,
|
||||
kind: Some(register::RegistrationKind::Guest),
|
||||
password: None,
|
||||
username: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let session = Session {
|
||||
// since we supply inhibit_login: false above, the access token needs to be there
|
||||
// TODO: maybe unwrap is not the best solution though
|
||||
access_token: response.access_token.unwrap(),
|
||||
identification: Some(Identification {
|
||||
// same as access_token
|
||||
device_id: response.device_id.unwrap(),
|
||||
user_id: response.user_id,
|
||||
}),
|
||||
};
|
||||
*self.0.session.lock().unwrap() = Some(session.clone());
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Register as a new user on this server.
|
||||
///
|
||||
/// In contrast to `api::r0::account::register::call()`, this method stores
|
||||
/// the session data returned by the endpoint in this client, instead of
|
||||
/// returning it.
|
||||
///
|
||||
/// The username is the local part of the returned user_id. If it is
|
||||
/// omitted from this request, the server will generate one.
|
||||
pub async fn register_user(
|
||||
&self,
|
||||
username: Option<String>,
|
||||
password: String,
|
||||
) -> Result<Session, Error<api::r0::uiaa::UiaaResponse>> {
|
||||
use api::r0::account::register;
|
||||
|
||||
let response = self
|
||||
.request(register::Request {
|
||||
auth: None,
|
||||
device_id: None,
|
||||
inhibit_login: false,
|
||||
initial_device_display_name: None,
|
||||
kind: Some(register::RegistrationKind::User),
|
||||
password: Some(password),
|
||||
username,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let session = Session {
|
||||
// since we supply inhibit_login: false above, the access token needs to be there
|
||||
// TODO: maybe unwrap is not the best solution though
|
||||
access_token: response.access_token.unwrap(),
|
||||
identification: Some(Identification {
|
||||
// same as access_token
|
||||
device_id: response.device_id.unwrap(),
|
||||
user_id: response.user_id,
|
||||
}),
|
||||
};
|
||||
*self.0.session.lock().unwrap() = Some(session.clone());
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Convenience method that represents repeated calls to the sync_events endpoint as a stream.
|
||||
///
|
||||
/// If the since parameter is None, the first Item might take a significant time to arrive and
|
||||
/// be deserialized, because it contains all events that have occurred in the whole lifetime of
|
||||
/// the logged-in users account and are visible to them.
|
||||
pub fn sync(
|
||||
&self,
|
||||
filter: Option<api::r0::sync::sync_events::Filter>,
|
||||
since: Option<String>,
|
||||
set_presence: ruma_common::presence::PresenceState,
|
||||
timeout: Option<Duration>,
|
||||
) -> impl Stream<Item = Result<api::r0::sync::sync_events::Response, Error<api::Error>>>
|
||||
+ TryStream<Ok = api::r0::sync::sync_events::Response, Error = Error<api::Error>> {
|
||||
use api::r0::sync::sync_events;
|
||||
|
||||
let client = self.clone();
|
||||
stream::try_unfold(since, move |since| {
|
||||
let client = client.clone();
|
||||
let filter = filter.clone();
|
||||
|
||||
async move {
|
||||
let response = client
|
||||
.request(sync_events::Request {
|
||||
filter,
|
||||
since,
|
||||
full_state: false,
|
||||
set_presence,
|
||||
timeout,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let next_batch_clone = response.next_batch.clone();
|
||||
Ok(Some((response, Some(next_batch_clone))))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Makes a request to a Matrix API endpoint.
|
||||
pub fn request<Request: Endpoint>(
|
||||
&self,
|
||||
request: Request,
|
||||
) -> impl Future<Output = Result<Request::Response, Error<Request::ResponseError>>> {
|
||||
self.request_with_url_params(request, None)
|
||||
}
|
||||
|
||||
/// Makes a request to a Matrix API endpoint including additional URL parameters.
|
||||
pub fn request_with_url_params<Request: Endpoint>(
|
||||
&self,
|
||||
request: Request,
|
||||
params: Option<BTreeMap<String, String>>,
|
||||
) -> impl Future<Output = Result<Request::Response, Error<Request::ResponseError>>> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let mut url = client.homeserver_url.clone();
|
||||
|
||||
async move {
|
||||
let mut hyper_request = request.try_into()?.map(hyper::Body::from);
|
||||
|
||||
{
|
||||
let uri = hyper_request.uri();
|
||||
|
||||
url.set_path(uri.path());
|
||||
url.set_query(uri.query());
|
||||
|
||||
if let Some(params) = params {
|
||||
for (key, value) in params {
|
||||
url.query_pairs_mut().append_pair(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
if Request::METADATA.requires_authentication {
|
||||
if let Some(ref session) = *client.session.lock().unwrap() {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("access_token", &session.access_token);
|
||||
} else {
|
||||
return Err(Error::AuthenticationRequired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*hyper_request.uri_mut() = Uri::from_str(url.as_ref())?;
|
||||
|
||||
let hyper_response = client.hyper.request(hyper_request).await?;
|
||||
let (head, body) = hyper_response.into_parts();
|
||||
|
||||
// FIXME: We read the response into a contiguous buffer here (not actually required for
|
||||
// deserialization) and then copy the whole thing to convert from Bytes to Vec<u8>.
|
||||
let full_body = hyper::body::to_bytes(body).await?;
|
||||
let full_response = HttpResponse::from_parts(head, full_body.as_ref().to_owned());
|
||||
|
||||
Ok(Request::Response::try_from(full_response)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Clone for Client<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
57
ruma-client/src/session.rs
Normal file
57
ruma-client/src/session.rs
Normal file
@ -0,0 +1,57 @@
|
||||
//! User sessions.
|
||||
|
||||
use ruma_identifiers::{DeviceId, UserId};
|
||||
|
||||
/// A user session, containing an access token and information about the associated user account.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)]
|
||||
pub struct Session {
|
||||
/// The access token used for this session.
|
||||
pub access_token: String,
|
||||
/// Identification information for a user
|
||||
pub identification: Option<Identification>,
|
||||
}
|
||||
|
||||
/// The identification information about the associated user account if the session is associated with
|
||||
/// a single user account.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)]
|
||||
pub struct Identification {
|
||||
/// The user the access token was issued for.
|
||||
pub user_id: UserId,
|
||||
/// The ID of the client device
|
||||
pub device_id: Box<DeviceId>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Create a new user session from an access token and a user ID.
|
||||
#[deprecated]
|
||||
pub fn new(access_token: String, user_id: UserId, device_id: Box<DeviceId>) -> Self {
|
||||
Self {
|
||||
access_token,
|
||||
identification: Some(Identification { user_id, device_id }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the access token associated with this session.
|
||||
#[deprecated]
|
||||
pub fn access_token(&self) -> &str {
|
||||
&self.access_token
|
||||
}
|
||||
|
||||
/// Get the ID of the user the session belongs to.
|
||||
#[deprecated]
|
||||
pub fn user_id(&self) -> Option<&UserId> {
|
||||
if let Some(identification) = &self.identification {
|
||||
return Some(&identification.user_id);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get ID of the device the session belongs to.
|
||||
#[deprecated]
|
||||
pub fn device_id(&self) -> Option<&DeviceId> {
|
||||
if let Some(identification) = &self.identification {
|
||||
return Some(&identification.device_id);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user