Initial commit.

master
Damir Jelić 2019-10-20 13:56:46 +02:00
commit 4989108324
8 changed files with 486 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/Cargo.lock
/target

32
Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
description = "A high level Matrix client library."
edition = "2018"
homepage = "https://github.com/poljar/nio-rust"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "MIT"
name = "matrix-nio"
readme = "README.md"
repository = "https://github.com/poljar/nio-rust"
version = "0.1.0"
[dependencies]
js_int = "*"
futures-preview = "0.3.0-alpha.18"
reqwest = "0.10.0-alpha.1"
http = "0.1.18"
ruma-api = "*"
ruma-client-api = { git = "https://github.com/ruma/ruma-client-api" }
ruma-events = "0.12"
ruma-identifiers = "*"
serde_json = "1.0.40"
serde_urlencoded = "0.6.1"
url = "2.1.0"
[dependencies.serde]
version = "1.0.101"
features = ["derive"]
[dev-dependencies]
tokio = "0.2.0-alpha.6"

21
LICENSE Normal file
View File

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

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# nio-rust
**nio-rust** is a port of the [matrix-nio][] python library to [Rust][].
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
[matrix-nio]: https://github.com/poljar/matrix-nio
## Status
This library is very much work in progress.
## License
[MIT](http://opensource.org/licenses/MIT)

58
examples/login.rs Normal file
View File

@ -0,0 +1,58 @@
use std::{env, process::exit};
use matrix_nio::{
self,
events::{
collections::all::RoomEvent,
room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
},
AsyncClient, AsyncClientConfig, SyncSettings,
};
async fn login(
homeserver_url: String,
username: String,
password: String,
) -> Result<(), matrix_nio::Error> {
let client_config = AsyncClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification();
let mut client = AsyncClient::new_with_config(&homeserver_url, None, client_config).unwrap();
client.login(username, password, None).await?;
let response = client.sync(SyncSettings::new()).await?;
for (room_id, room) in response.rooms.join {
println!("Room {}", room_id);
for event in room.timeline.events {
if let RoomEvent::RoomMessage(MessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
sender,
..
}) = event
{
println!("{}: {}", sender, msg_body);
}
}
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_nio::Error> {
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)
}
};
login(homeserver_url, username, password).await
}

81
src/error.rs Normal file
View File

@ -0,0 +1,81 @@
//! Error conditions.
use std::error::Error as StdError;
use std::fmt::{Display, Formatter, Result as FmtResult};
use http::uri::InvalidUri;
use reqwest::Error as ReqwestError;
use ruma_api::Error as RumaApiError;
use serde_json::Error as SerdeJsonError;
use serde_urlencoded::ser::Error as SerdeUrlEncodedSerializeError;
/// An error that can occur during client operations.
#[derive(Debug)]
pub struct Error(pub(crate) InnerError);
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let message = match self.0 {
InnerError::AuthenticationRequired => "The queried endpoint requires authentication but was called with an anonymous client.",
InnerError::Reqwest(_) => "An HTTP error occurred.",
InnerError::ConfigurationError(_) => "Error configuring the client",
InnerError::Uri(_) => "Provided string could not be converted into a URI.",
InnerError::RumaApi(_) => "An error occurred converting between ruma_client_api and hyper types.",
InnerError::SerdeJson(_) => "A serialization error occurred.",
InnerError::SerdeUrlEncodedSerialize(_) => "An error occurred serializing data to a query string.",
};
write!(f, "{}", message)
}
}
impl StdError for Error {}
/// Internal representation of errors.
#[derive(Debug)]
pub(crate) enum InnerError {
/// Queried endpoint requires authentication but was called on an anonymous client.
AuthenticationRequired,
/// An error in the client configuration.
ConfigurationError(String),
/// An error at the HTTP layer.
Reqwest(ReqwestError),
/// An error when parsing a string as a URI.
Uri(InvalidUri),
/// An error converting between ruma_client_api types and Hyper types.
RumaApi(RumaApiError),
/// An error when serializing or deserializing a JSON value.
SerdeJson(SerdeJsonError),
/// An error when serializing a query string value.
SerdeUrlEncodedSerialize(SerdeUrlEncodedSerializeError),
}
impl From<InvalidUri> for Error {
fn from(error: InvalidUri) -> Self {
Self(InnerError::Uri(error))
}
}
impl From<RumaApiError> for Error {
fn from(error: RumaApiError) -> Self {
Self(InnerError::RumaApi(error))
}
}
impl From<SerdeJsonError> for Error {
fn from(error: SerdeJsonError) -> Self {
Self(InnerError::SerdeJson(error))
}
}
impl From<SerdeUrlEncodedSerializeError> for Error {
fn from(error: SerdeUrlEncodedSerializeError) -> Self {
Self(InnerError::SerdeUrlEncodedSerialize(error))
}
}
impl From<ReqwestError> for Error {
fn from(error: ReqwestError) -> Self {
Self(InnerError::Reqwest(error))
}
}

233
src/lib.rs Normal file
View File

@ -0,0 +1,233 @@
//! Crate `ruma_client` is a [Matrix](https://matrix.org/) client library.
//!
use std::convert::{TryFrom, TryInto};
use http::Method as HttpMethod;
use http::Response as HttpResponse;
use js_int::UInt;
use reqwest;
use ruma_api::Endpoint;
use url::Url;
use crate::error::InnerError;
pub use crate::{error::Error, session::Session};
pub use ruma_client_api as api;
pub use ruma_events as events;
//pub mod api;
mod error;
mod session;
#[derive(Debug)]
pub struct AsyncClient {
/// The URL of the homeserver to connect to.
homeserver: Url,
/// The underlying HTTP client.
client: reqwest::Client,
/// User session data.
session: Option<Session>,
}
#[derive(Default, Debug)]
pub struct AsyncClientConfig {
proxy: Option<reqwest::Proxy>,
use_sys_proxy: bool,
disable_ssl_verification: bool,
}
impl AsyncClientConfig {
pub fn new() -> Self {
Default::default()
}
pub fn proxy(mut self, proxy: &str) -> Result<Self, Error> {
if self.use_sys_proxy {
return Err(Error(InnerError::ConfigurationError(
"Using the system proxy has been previously configured.".to_string(),
)));
}
self.proxy = Some(reqwest::Proxy::all(proxy)?);
Ok(self)
}
pub fn use_sys_proxy(mut self) -> Result<Self, Error> {
if self.proxy.is_some() {
return Err(Error(InnerError::ConfigurationError(
"A proxy has already been configured.".to_string(),
)));
}
self.use_sys_proxy = true;
Ok(self)
}
pub fn disable_ssl_verification(mut self) -> Self {
self.disable_ssl_verification = true;
self
}
}
#[derive(Debug, Default)]
pub struct SyncSettings {
pub(crate) timeout: Option<UInt>,
pub(crate) token: Option<String>,
pub(crate) full_state: Option<bool>,
}
impl SyncSettings {
pub fn new() -> Self {
Default::default()
}
pub fn token<S: Into<String>>(mut self, token: S) -> Self {
self.token = Some(token.into());
self
}
pub fn timeout<T: TryInto<UInt>>(mut self, timeout: T) -> Result<Self, js_int::TryFromIntError>
where
js_int::TryFromIntError:
std::convert::From<<T as std::convert::TryInto<js_int::UInt>>::Error>,
{
self.timeout = Some(timeout.try_into()?);
Ok(self)
}
pub fn full_state(mut self, full_state: bool) -> Self {
self.full_state = Some(full_state);
self
}
}
use api::r0::session::login;
use api::r0::sync::sync_events;
impl AsyncClient {
/// Creates a new client for making HTTP requests to the given homeserver.
pub fn new(homeserver_url: &str, session: Option<Session>) -> Result<Self, url::ParseError> {
let homeserver = Url::parse(homeserver_url)?;
let client = reqwest::Client::new();
Ok(Self {
homeserver,
client,
session,
})
}
pub fn new_with_config(
homeserver_url: &str,
session: Option<Session>,
config: AsyncClientConfig,
) -> Result<Self, url::ParseError> {
let homeserver = Url::parse(homeserver_url)?;
let client = reqwest::Client::builder();
let client = if config.disable_ssl_verification {
client.danger_accept_invalid_certs(true)
} else {
client
};
let client = match config.proxy {
Some(p) => client.proxy(p),
None => client,
};
let client = if config.use_sys_proxy {
client.use_sys_proxy()
} else {
client
};
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_static("ruma"));
let client = client.default_headers(headers).build().unwrap();
Ok(Self {
homeserver,
client,
session,
})
}
pub async fn login<S: Into<String>>(
&mut self,
user: S,
password: S,
device_id: Option<S>,
) -> Result<login::Response, Error> {
let request = login::Request {
address: None,
login_type: login::LoginType::Password,
medium: None,
device_id: device_id.map(|d| d.into()),
password: password.into(),
user: user.into(),
};
let response = self.send(request).await.unwrap();
let session = Session {
access_token: response.access_token.clone(),
device_id: response.device_id.clone(),
user_id: response.user_id.clone(),
};
self.session = Some(session.clone());
Ok(response)
}
pub async fn sync(&self, sync_settings: SyncSettings) -> Result<sync_events::Response, Error> {
let request = sync_events::Request {
filter: None,
since: sync_settings.token,
full_state: sync_settings.full_state,
set_presence: None,
timeout: sync_settings.timeout,
};
let response = self.send(request).await.unwrap();
Ok(response)
}
async fn send<Request: Endpoint>(&self, request: Request) -> Result<Request::Response, Error> {
let request: http::Request<Vec<u8>> = request.try_into()?;
let url = request.uri();
let url = self.homeserver.join(url.path()).unwrap();
let request_builder = match Request::METADATA.method {
HttpMethod::GET => self.client.get(url),
HttpMethod::POST => {
let body = request.body().clone();
self.client.post(url).body(body)
}
HttpMethod::PUT => unimplemented!(),
HttpMethod::DELETE => unimplemented!(),
_ => panic!("Unsuported method"),
};
let request_builder = if Request::METADATA.requires_authentication {
if let Some(ref session) = self.session {
request_builder.bearer_auth(&session.access_token)
} else {
return Err(Error(InnerError::AuthenticationRequired));
}
} else {
request_builder
};
let response = request_builder.send().await?;
let status = response.status();
let body = response.bytes().await?.as_ref().to_owned();
let response = HttpResponse::builder().status(status).body(body).unwrap();
let response = Request::Response::try_from(response)?;
Ok(response)
}
}

44
src/session.rs Normal file
View File

@ -0,0 +1,44 @@
//! User sessions.
use ruma_identifiers::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,
/// The user the access token was issued for.
pub user_id: UserId,
/// The ID of the client device
pub device_id: String,
}
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: String) -> Self {
Self {
access_token,
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) -> &UserId {
&self.user_id
}
/// Get ID of the device the session belongs to.
#[deprecated]
pub fn device_id(&self) -> &str {
&self.device_id
}
}