//! Mattermost's REST calls, one connection per request, with the token in the `Authorization` //! header. A 429 waits as asked; a 5xx is tried twice more; a 401 or 403 is `Auth` at once. use std::time::{Duration, SystemTime}; use bxcore::secrets::Secret; use serde::Deserialize; use crate::config::valid_id; use crate::http::{self, Response, rate_limit_wait}; use crate::mm::{Me, MmError, Post, Since, json, since_list}; use crate::net::Connector; use crate::ws::conn::host_header; /// The pause before trying a call again after a 5xx. pub const RETRY_5XX: Duration = Duration::from_millis(500); /// How many times a call is tried again after a 5xx, and waits after a 429. pub const RETRIES: u32 = 2; /// How much of an error body is kept for the message. const BODY_KEPT: usize = 200; pub struct Client { connector: Connector, token: Secret, timeout: Duration, } #[derive(Deserialize)] struct Channel { id: String, } #[derive(Deserialize)] struct User { username: String, } /// A Mattermost username: 1 to 64 characters of `a-z`, `0-9`, `.`, `-` and `_`. fn valid_username(name: &str) -> bool { (1..=64).contains(&name.len()) && name.bytes().all(|b| { b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'-' | b'_') }) } impl Client { /// `timeout` bounds connecting and each read: no bytes for that long is an error. pub fn new(connector: Connector, token: Secret, timeout: Duration) -> Client { // Store the three. Client { connector, token, timeout, } } pub fn connector(&self) -> &Connector { // The Connector. &self.connector } pub fn token(&self) -> &Secret { // The Secret. &self.token } fn once(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result { // 1. `self.connector.connect(self.timeout)`, then `set_read_timeout(Some(self.timeout))`. let mut stream = self .connector .connect(self.timeout) .map_err(|e| MmError::Net(format!("{method} {path}: {e}")))?; stream .set_read_timeout(Some(self.timeout)) .map_err(|e| MmError::Net(format!("{method} {path}: {e}")))?; // 2. Headers: Authorization "Bearer " (the only use of `expose`), Accept // "application/json", and Content-Type "application/json" when there is a body. Host is // `host_header(self.connector.server())`. let auth = format!("Bearer {}", self.token.expose()); let mut headers: Vec<(&str, &str)> = vec![ ("Authorization", auth.as_str()), ("Accept", "application/json"), ]; if body.is_some() { headers.push(("Content-Type", "application/json")); } // 3. `http::request`. Every error on the way is Net(" : "). http::request( &mut stream, method, &host_header(self.connector.server()), path, &headers, body, ) .map_err(|e| MmError::Net(format!("{method} {path}: {e}"))) } /// One call, tried again as the module comment says; the body of a 2xx answer. fn call(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result, MmError> { // Loop over `once`: 2xx -> the body. 401 or 403 -> Auth(status) at once. 429 -> after // RETRIES waits already, RateLimited(wait); else sleep `rate_limit_wait(&head, // SystemTime::now())` and try again. 5xx -> up to RETRIES more tries, sleeping RETRY_5XX // before each. Anything else -> Status(status, the first BODY_KEPT characters of the body, // lossy UTF-8). let mut tries: u32 = 0; loop { let answer = self.once(method, path, body)?; let status = answer.head.status; if (200..300).contains(&status) { return Ok(answer.body); } match status { 401 | 403 => return Err(MmError::Auth(status)), 429 => { let wait = rate_limit_wait(&answer.head, SystemTime::now()); if tries < RETRIES { tries += 1; std::thread::sleep(wait); continue; } return Err(MmError::RateLimited(wait)); } _ if status >= 500 => { if tries < RETRIES { tries += 1; std::thread::sleep(RETRY_5XX); continue; } return Err(status_error(status, &answer.body)); } _ => return Err(status_error(status, &answer.body)), } } } /// `GET /api/v4/users/me`: who this token is. pub fn me(&self) -> Result { // GET /api/v4/users/me into Me; an id that is not `valid_id` or an empty username is Json. let body = self.call("GET", "/api/v4/users/me", None)?; let me: Me = json(&body)?; if !valid_id(&me.id) || me.username.is_empty() { return Err(MmError::Json( "the user me has no id or no username".to_string(), )); } Ok(me) } /// `POST /api/v4/posts`: a post in `channel`, in the thread of `root` (empty: top level). pub fn create_post(&self, channel: &str, root: &str, message: &str) -> Result { // POST /api/v4/posts with {"channel_id", "root_id", "message"}; the answer is a Post, // `check`ed. let payload = serde_json::json!({ "channel_id": channel, "root_id": root, "message": message }) .to_string(); let body = self.call("POST", "/api/v4/posts", Some(payload.as_bytes()))?; let post: Post = json(&body)?; post.check() } /// `GET /api/v4/channels/{channel}/posts?since=`: the posts created after `since`. pub fn posts_since(&self, channel: &str, since: i64) -> Result { // A channel that is not `valid_id` is Json, and nothing is sent. Otherwise GET // /api/v4/channels//posts?since=, through `since_list`. if !valid_id(channel) { return Err(MmError::Json( "a channel path must be a Mattermost id".to_string(), )); } let body = self.call( "GET", &format!("/api/v4/channels/{channel}/posts?since={since}"), None, )?; since_list(&body, since) } /// `POST /api/v4/channels/direct`: the id of the direct channel between two users. pub fn direct_channel(&self, a: &str, b: &str) -> Result { // POST /api/v4/channels/direct with the JSON array [a, b]; the answer has an "id", which // must be `valid_id` (else Json). let payload = serde_json::to_vec(&serde_json::json!([a, b])) .map_err(|e| MmError::Json(e.to_string()))?; let body = self.call("POST", "/api/v4/channels/direct", Some(payload.as_slice()))?; let channel: Channel = json(&body)?; if !valid_id(&channel.id) { return Err(MmError::Json("the direct channel has no id".to_string())); } Ok(channel.id) } /// `PUT /api/v4/posts/{post}/patch`: replace the post's text. pub fn patch_post(&self, post: &str, message: &str) -> Result<(), MmError> { // A post that is not `valid_id` is Json, and nothing is sent. Otherwise PUT // /api/v4/posts//patch with {"message": message}, through `call`; the answer is a // Post, `check`ed and dropped. if !valid_id(post) { return Err(MmError::Json( "a post path must be a Mattermost id".to_string(), )); } let payload = serde_json::json!({ "message": message }).to_string(); let body = self.call( "PUT", &format!("/api/v4/posts/{post}/patch"), Some(payload.as_bytes()), )?; json::(&body)?.check().map(|_| ()) } /// `GET /api/v4/users/{user}`: the user's username. pub fn username(&self, user: &str) -> Result { // A user that is not `valid_id` is Json. Otherwise GET /api/v4/users/; a username // that is not `valid_username` is Json. if !valid_id(user) { return Err(MmError::Json( "a user path must be a Mattermost id".to_string(), )); } let body = self.call("GET", &format!("/api/v4/users/{user}"), None)?; let user: User = json(&body)?; if !valid_username(&user.username) { return Err(MmError::Json("the user has no valid username".to_string())); } Ok(user.username) } } fn status_error(status: u16, body: &[u8]) -> MmError { // Any non-2xx, non-4xx-that-isn't-401/403/429/5xx: the status and the first `BODY_KEPT` // characters, lossy UTF-8. MmError::Status( status, String::from_utf8_lossy(body) .chars() .take(BODY_KEPT) .collect(), ) }