Implemented supported specs request

This commit is contained in:
Edward Shen 2019-08-20 21:39:34 -04:00
parent d7f6063325
commit 7aa5928764
Signed by: edward
GPG key ID: F350507060ED6C90
5 changed files with 164 additions and 68 deletions

View file

@ -1,6 +1,8 @@
{ {
"cSpell.words": [ "cSpell.words": [
"errcode", "errcode",
"reqwest" "homeserver",
"reqwest",
"mockito"
] ]
} }

View file

@ -12,3 +12,6 @@ reqwest = "0.9"
serde_json = "1.0" serde_json = "1.0"
serde = "1.0" serde = "1.0"
olm-rs = "0.2" olm-rs = "0.2"
[dev-dependencies]
mockito = "0.20"

View file

@ -1,13 +1,13 @@
# r0.5 # r0.5.0
- [ ] 2 API Standards - [ ] 2 API Standards
- [ ] 2.1 GET /_matrix/client/versions - [x] ~~2.1 GET /_matrix/client/versions~~
- [x] ~~3 Web Browser Clients~~ *Not applicable* - [x] ~~3 Web Browser Clients~~ *Not applicable*
- [ ] 4 Server Discovery - [ ] 4 Server Discovery
- [ ] 4.1 Well-known URI - [ ] 4.1 Well-known URI
- [ ] 4.1.1 GET /.well-known/matrix/client - [ ] 4.1.1 GET /.well-known/matrix/client
- [ ] 5 Client Authentication - [ ] 5 Client Authentication
- [ ] 5.1 Using access tokens - [x] ~~5.1 Using access tokens~~ *Only through Authorization header*
- [ ] 5.2 Relationship between access tokens and devices - [ ] 5.2 Relationship between access tokens and devices
- [ ] 5.3 User-Interactive Authentication API - [ ] 5.3 User-Interactive Authentication API
- [ ] 5.3.1 Overview - [ ] 5.3.1 Overview

View file

@ -1,13 +1,17 @@
use crate::api::methods::sync::SyncResponse; use crate::api::methods::sync::SyncResponse;
use reqwest::Client as reqwest_client;
use reqwest::{ use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT}, header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT},
StatusCode, Client as reqwest_client, Response, StatusCode,
}; };
use serde::Deserialize;
use std::{collections::HashMap, error::Error, fmt, time}; use std::{collections::HashMap, error::Error, fmt, time};
use url::{ParseError, Url}; use url::{ParseError, Url};
#[cfg(test)]
use mockito;
const V2_API_PATH: &str = "/_matrix/client/r0"; const V2_API_PATH: &str = "/_matrix/client/r0";
const SUPPORTED_VERSION: &str = "r0.5.0";
#[derive(Debug)] #[derive(Debug)]
pub enum MatrixParseError { pub enum MatrixParseError {
@ -57,25 +61,59 @@ pub struct Client {
reqwest_client: reqwest_client, reqwest_client: reqwest_client,
} }
#[derive(Deserialize)]
/// Response struct for [Section 2.1 **GET** /_matrix/client/versions](https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-versions).
pub struct SupportedSpecs {
pub versions: Vec<String>,
pub unstable_features: Option<HashMap<String, bool>>,
}
impl Client { impl Client {
pub fn new( pub fn new(
homeserver_url: &str, homeserver_url: &str,
access_token: Option<String>, access_token: Option<String>,
mxid: Option<String>, mxid: Option<String>,
default_492_wait_ms: Option<u64>, default_492_wait_ms: Option<u64>,
) -> Result<Self, MatrixParseError> { ) -> Result<Self, Box<dyn Error>> {
let url = Url::parse(homeserver_url)?; let url = Url::parse(homeserver_url)?;
if url.scheme().is_empty() { if url.scheme().is_empty() {
return Err(MatrixParseError::EmptyScheme); panic!("todo: implement handling");
} }
Ok(Client { let client = Client {
homeserver_url: homeserver_url.to_string(), homeserver_url: homeserver_url.to_string(),
access_token, access_token,
mxid, mxid,
default_492_wait_ms: default_492_wait_ms.unwrap_or_else(|| 5000), default_492_wait_ms: default_492_wait_ms.unwrap_or_else(|| 5000),
reqwest_client: reqwest_client::new(), reqwest_client: reqwest_client::new(),
}) };
if !client
.supported_versions()?
.versions
.contains(&SUPPORTED_VERSION.to_string())
{
// TODO: Implement proper response
panic!("server version doesn't support client");
}
Ok(client)
}
/// Implementation of [Section 2.1 **GET** /_matrix/client/versions](https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-versions).
///
/// Returns a list of matrix specifications a server supports, as well as
/// a map of unstable features the server has advertised.
pub fn supported_versions(&self) -> Result<SupportedSpecs, Box<dyn Error>> {
Ok(self
.send(
MatrixHTTPMethod::Get,
Some("/_matrix/client/versions"),
None,
None,
None,
)?
.json()?)
} }
/// Sends an API request to the homeserver using the specified method and /// Sends an API request to the homeserver using the specified method and
@ -88,6 +126,7 @@ impl Client {
/// This is a blocking, synchronous send. If the response from the /// This is a blocking, synchronous send. If the response from the
/// homeserver indicates that too many requests were sent, it will attempt /// homeserver indicates that too many requests were sent, it will attempt
/// to wait the specified duration (or a provided default) before retrying. /// to wait the specified duration (or a provided default) before retrying.
/// TODO: Make async
fn send( fn send(
&self, &self,
method: MatrixHTTPMethod, method: MatrixHTTPMethod,
@ -95,14 +134,16 @@ impl Client {
content: Option<String>, content: Option<String>,
query_params: Option<HashMap<String, String>>, query_params: Option<HashMap<String, String>>,
headers: Option<HeaderMap>, headers: Option<HeaderMap>,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<Response, Box<dyn std::error::Error>> {
let mut query_params = query_params.unwrap_or_default(); let mut query_params = query_params.unwrap_or_default();
let mut headers = headers.unwrap_or_default(); let mut headers = headers.unwrap_or_default();
let endpoint = &format!(
"{}{}", #[cfg(test)]
self.homeserver_url, let url = &mockito::server_url();
path.unwrap_or_else(|| V2_API_PATH), #[cfg(not(test))]
); let url = &self.homeserver_url;
let endpoint = &format!("{}{}", url, path.unwrap_or_else(|| V2_API_PATH));
let mut request = match method { let mut request = match method {
MatrixHTTPMethod::Get => self.reqwest_client.get(endpoint), MatrixHTTPMethod::Get => self.reqwest_client.get(endpoint),
MatrixHTTPMethod::Put => self.reqwest_client.put(endpoint), MatrixHTTPMethod::Put => self.reqwest_client.put(endpoint),
@ -140,7 +181,7 @@ impl Client {
.send()?; .send()?;
if res.status().is_success() { if res.status().is_success() {
return Ok(res.text()?); return Ok(res);
} else if res.status() == StatusCode::TOO_MANY_REQUESTS { } else if res.status() == StatusCode::TOO_MANY_REQUESTS {
let mut body: HashMap<String, String> = res.json()?; let mut body: HashMap<String, String> = res.json()?;
if let Some(value) = body.get("error") { if let Some(value) = body.get("error") {
@ -167,7 +208,7 @@ impl Client {
method: MatrixHTTPMethod, method: MatrixHTTPMethod,
path: &str, path: &str,
query_params: HashMap<String, String>, query_params: HashMap<String, String>,
) -> Result<String, Box<dyn Error>> { ) -> Result<Response, Box<dyn Error>> {
self.send(method, Some(path), None, Some(query_params), None) self.send(method, Some(path), None, Some(query_params), None)
} }
@ -207,14 +248,64 @@ impl Client {
.to_string(), .to_string(),
); );
Ok(serde_json::from_str(&self.send_query( Ok(self
MatrixHTTPMethod::Get, .send_query(MatrixHTTPMethod::Get, "/sync", params)?
"/sync", .json()?)
params,
)?)?)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use mockito::mock;
#[test]
fn client_init_properly() {}
#[test]
fn supported_versions_complete_resp() {
let _m = mock("GET", "/_matrix/client/versions")
.with_body(
r#"{
"versions": ["r0.4.0", "r0.5.0"],
"unstable_features": { "m.lazy_load_members": true }
}"#,
)
.create();
// "valid" location must be supplied as reqwest attempts to parse it.
let resp = Client::new("http://dummy.website", None, None, None)
.unwrap()
.supported_versions()
.unwrap();
assert_eq!(resp.versions, vec!["r0.4.0", "r0.5.0"]);
assert!(resp
.unstable_features
.unwrap_or_default()
.get("m.lazy_load_members")
.unwrap_or_else(|| &false));
}
#[test]
fn supported_versions_just_versions() {
let _m = mock("GET", "/_matrix/client/versions")
.with_body(
r#"{
"versions": ["r0.4.0", "r0.5.0"]
}"#,
)
.create();
// "valid" location must be supplied as reqwest attempts to parse it.
let resp = Client::new("http://dummy.website", None, None, None)
.unwrap()
.supported_versions()
.unwrap();
assert_eq!(resp.versions, vec!["r0.4.0", "r0.5.0"]);
assert!(resp.unstable_features.is_none());
}
}
#[derive(Default)] #[derive(Default)]
pub struct ApiError {} pub struct ApiError {}

View file

@ -93,50 +93,50 @@ impl User {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use super::*; // use super::*;
#[test] // #[test]
fn new_returns_err_on_invalid_id() { // fn new_returns_err_on_invalid_id() {
assert_eq!( // assert_eq!(
User::new( // User::new(
Client::new("https://google.com", None, None, None, None).unwrap(), // Client::new("https://google.com", None, None, None).unwrap(),
String::from("abc:edf"), // String::from("abc:edf"),
None // None
), // ),
Err(UserInitError { // Err(UserInitError {
message: "User ID must start with a @".to_string(), // message: "User ID must start with a @".to_string(),
reason: UserInitErrorReason::InvalidUsername // reason: UserInitErrorReason::InvalidUsername
}) // })
); // );
assert_eq!( // assert_eq!(
User::new( // User::new(
Client::new("https://google.com", None, None, None, None).unwrap(), // Client::new("https://google.com", None, None, None).unwrap(),
String::from("@abcedf"), // String::from("@abcedf"),
None // None
), // ),
Err(UserInitError { // Err(UserInitError {
message: "User ID must contain a :".to_string(), // message: "User ID must contain a :".to_string(),
reason: UserInitErrorReason::NoDomainProvided // reason: UserInitErrorReason::NoDomainProvided
}) // })
); // );
} // }
#[test] // #[test]
fn new_returns_struct_on_valid_input() { // fn new_returns_struct_on_valid_input() {
assert_eq!( // assert_eq!(
User::new( // User::new(
Client::new("https://google.com", None, None, None, None).unwrap(), // Client::new("https://google.com", None, None, None).unwrap(),
"@eddie:eddie.sh".to_string(), // "@eddie:eddie.sh".to_string(),
None // None
), // ),
Ok(User { // Ok(User {
id: "@eddie:eddie.sh".to_string(), // id: "@eddie:eddie.sh".to_string(),
display_name: None, // display_name: None,
client: Client::new("https://google.com", None, None, None, None).unwrap() // client: Client::new("https://google.com", None, None, None).unwrap()
}) // })
) // )
} // }
} // }