Initial commit.
commit
4989108324
|
@ -0,0 +1,2 @@
|
||||||
|
/Cargo.lock
|
||||||
|
/target
|
|
@ -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"
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue