use crate::api::methods::sync::SyncResponse; use reqwest::Client as reqwest_client; use reqwest::{ header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT}, StatusCode, }; use std::{collections::HashMap, error::Error, fmt, time}; use url::{ParseError, Url}; const V2_API_PATH: &str = "/_matrix/client/r0"; #[derive(Debug)] pub enum MatrixParseError { ParseError(ParseError), EmptyScheme, } pub enum PresenceState { Offline, Online, Unavailable, } impl From for MatrixParseError { fn from(parse_error: ParseError) -> Self { MatrixParseError::ParseError(parse_error) } } #[derive(Debug)] pub struct ResponseError { code: StatusCode, content: String, } impl fmt::Display for ResponseError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "{}: {}", self.code, self.content) } } impl Error for ResponseError {} pub enum MatrixHTTPMethod { Get, Put, Delete, Post, } #[derive(Debug)] pub struct Client { homeserver_url: String, access_token: Option, mxid: Option, default_492_wait_ms: u64, use_auth_header: bool, reqwest_client: reqwest_client, } impl Client { pub fn new( homeserver_url: &str, access_token: Option, mxid: Option, default_492_wait_ms: Option, use_auth_header: Option, ) -> Result { let url = Url::parse(homeserver_url)?; if url.scheme().is_empty() { return Err(MatrixParseError::EmptyScheme); } Ok(Client { homeserver_url: homeserver_url.to_string(), access_token, mxid, default_492_wait_ms: default_492_wait_ms.unwrap_or_else(|| 5000), use_auth_header: use_auth_header.unwrap_or_else(|| true), reqwest_client: reqwest_client::new(), }) } /// Sends an API request to the homeserver using the specified method and /// path, returning either an error or the response text. /// /// The header will automatically be populated with a user agent and have /// the content type set to `application/json`. If a token was provided, it /// will be used as a bearer auth header or as a query. /// /// This is a blocking, synchronous send. If the response from the /// homeserver indicates that too many requests were sent, it will attempt /// to wait the specified duration (or a provided default) before retrying. fn send( &self, method: MatrixHTTPMethod, path: Option<&str>, content: Option, query_params: Option>, headers: Option, ) -> Result> { let mut query_params = query_params.unwrap_or_default(); let mut headers = headers.unwrap_or_default(); let endpoint = &format!( "{}{}", self.homeserver_url, path.unwrap_or_else(|| V2_API_PATH), ); let mut request = match method { MatrixHTTPMethod::Get => self.reqwest_client.get(endpoint), MatrixHTTPMethod::Put => self.reqwest_client.put(endpoint), MatrixHTTPMethod::Delete => self.reqwest_client.delete(endpoint), MatrixHTTPMethod::Post => self.reqwest_client.post(endpoint), }; if !headers.contains_key(&USER_AGENT) { let user_agent = &format!("libmatrix-client/{}", env!("CARGO_PKG_VERSION")); headers.insert(USER_AGENT, HeaderValue::from_str(user_agent)?); } if !headers.contains_key(&CONTENT_TYPE) { headers.insert(CONTENT_TYPE, HeaderValue::from_str("application/json")?); } if let Some(token) = &self.access_token { if self.use_auth_header { request = request.bearer_auth(token); } else { query_params.insert("access_token".to_string(), token.to_string()); } } if let Some(id) = &self.mxid { query_params.insert("user_id".to_string(), id.to_string()); } request = request.headers(headers).query(&query_params); if let Some(content) = content { request = request.body(content); } loop { let mut res = request .try_clone() .expect("Unable to clone request") .send()?; if res.status().is_success() { return Ok(res.text()?); } else if res.status() == StatusCode::TOO_MANY_REQUESTS { let mut body: HashMap = res.json()?; if let Some(value) = body.get("error") { body = serde_json::from_str(value)?; } if let Some(value) = body.get("retry_after_ms") { std::thread::sleep(time::Duration::from_millis(value.parse::()?)); } else { std::thread::sleep(time::Duration::from_millis(self.default_492_wait_ms)); } } else { return Err(Box::from(ResponseError { code: res.status(), content: res.text()?, })); } } } fn send_query( &self, method: MatrixHTTPMethod, path: &str, query_params: HashMap, ) -> Result> { self.send(method, Some(path), None, Some(query_params), None) } pub fn sync( &self, bookmark_token: Option<&str>, timeout_ms: Option, filter: Option<&str>, get_full_state: Option, set_presence: Option, ) -> Result> { let mut params: HashMap = HashMap::with_capacity(5); params.insert( "timeout".to_string(), timeout_ms.unwrap_or_else(|| 30000).to_string(), ); if let Some(token) = bookmark_token { params.insert("since".to_string(), token.to_string()); } if let Some(filter) = filter { params.insert("filter".to_string(), filter.to_string()); } if let Some(true) = get_full_state { params.insert("full_state".to_string(), "true".to_string()); } params.insert( "full_state".to_string(), match set_presence { Some(PresenceState::Online) => "online", Some(PresenceState::Unavailable) => "unavailable", None | Some(PresenceState::Offline) => "offline", } .to_string(), ); Ok(serde_json::from_str(&self.send_query( MatrixHTTPMethod::Get, "/sync", params, )?)?) } } #[derive(Default)] pub struct ApiError {} impl fmt::Display for ApiError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "an error occurred!") } }