lib/mattermost/
status.rs

1//! Module responsible for sending custom status change to mattermost.
2use crate::mattermost::BaseSession;
3use crate::utils::parse_from_hmstr;
4use anyhow::Result;
5use chrono::{DateTime, Local};
6use derivative::Derivative;
7use serde::{Deserialize, Serialize};
8use serde_json as json;
9use std::fmt;
10use thiserror::Error;
11use tracing::debug;
12
13/// Implement errors specific to `MMStatus`
14#[allow(missing_docs)]
15#[derive(Debug, Error)]
16pub enum MMSError {
17    #[error("Bad json data")]
18    BadJSONData(#[from] serde_json::error::Error),
19    #[error("HTTP request error")]
20    HTTPRequestError(#[from] ureq::Error),
21    #[error("Mattermost login error")]
22    LoginError(#[from] anyhow::Error),
23}
24
25/// Custom struct to serialize the HTTP POST data into a json objecting using serde_json
26/// For a description of these fields see the [MatterMost OpenApi sources](https://github.com/mattermost/mattermost-api-reference/blob/master/v4/source/status.yaml)
27#[derive(Derivative, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
28#[derivative(Debug)]
29pub struct MMStatus {
30    /// custom status text description
31    pub text: String,
32    /// custom status emoji name
33    pub emoji: String,
34    /// custom status duration
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub duration: Option<String>,
37    /// custom status expiration
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub expires_at: Option<DateTime<Local>>,
40}
41
42impl fmt::Display for MMStatus {
43    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44        write!(
45            f,
46            "{}:{} (duration: {:?}, expire at: {:?})",
47            self.emoji, self.text, self.duration, self.expires_at
48        )
49    }
50}
51
52impl MMStatus {
53    /// Create a `MMStatus` ready to be sent to the `mm_base_uri` mattermost instance.
54    /// Authentication is done with the private access `token`.
55    pub fn new(text: String, emoji: String) -> MMStatus {
56        MMStatus {
57            text,
58            emoji,
59            duration: None,
60            expires_at: None,
61        }
62    }
63    /// Add expiration time with the format "hh:mm" to the mattermost custom status
64    pub fn expires_at(&mut self, time_str: &Option<String>) {
65        // do not set expiry time if set in the past
66        if let Some(expiry) = parse_from_hmstr(time_str) {
67            if Local::now() < expiry {
68                self.expires_at = Some(expiry);
69                self.duration = Some("date_and_time".to_owned());
70            } else {
71                debug!("now {:?} >= expiry {:?}", Local::now(), expiry);
72            }
73        }
74        // let dt: NaiveDateTime = NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11);
75    }
76    /// This function is essentially used for debugging or testing
77    pub fn to_json(&self) -> Result<String, MMSError> {
78        json::to_string(&self).map_err(MMSError::BadJSONData)
79    }
80
81    /// Send self custom status once
82    #[allow(clippy::borrowed_box)] // Box needed beacause we can get two different types.
83    pub fn _send(&self, session: &Box<dyn BaseSession>) -> Result<ureq::Response, ureq::Error> {
84        let token = session
85            .token()
86            .expect("Internal Error: token is unset in current session");
87        let uri = session.base_uri().to_owned() + "/api/v4/users/me/status/custom";
88        ureq::put(&uri)
89            .set("Authorization", &("Bearer ".to_owned() + token))
90            .send_json(serde_json::to_value(&self).unwrap_or_else(|e| {
91                panic!(
92                    "Serialization of MMStatus '{:?}' failed with {:?}",
93                    &self, &e
94                )
95            }))
96    }
97    /// Send self custom status, trying to login once in case of 401 failure.
98    pub fn send(&mut self, session: &mut Box<dyn BaseSession>) -> Result<ureq::Response, MMSError> {
99        debug!("Post status: {}", self.to_owned().to_json()?);
100        match self._send(session) {
101            Ok(response) => Ok(response),
102            Err(ureq::Error::Status(code, response)) => {
103                /* the server returned an unexpected status
104                code (such as 400, 500 etc) */
105                if code == 401 {
106                    // relogin and retry
107                    session.login().map_err(MMSError::LoginError)?;
108                    self._send(session)
109                } else {
110                    Err(ureq::Error::Status(code, response))
111                }
112            }
113            Err(e) => Err(e),
114        }
115        .map_err(MMSError::HTTPRequestError)
116    }
117}
118
119#[cfg(test)]
120mod send_should {
121    use super::*;
122    use crate::mattermost::{BaseSession, Session};
123    use httpmock::prelude::*;
124    use test_log::test; // Automatically trace tests
125    #[test]
126    fn send_required_json() -> Result<()> {
127        // Start a lightweight mock server.
128        let server = MockServer::start();
129        let mut mmstatus = MMStatus::new("text".into(), "emoji".into());
130
131        // Create a mock on the server.
132        let server_mock = server.mock(|expect, resp_with| {
133            expect
134                .method(PUT)
135                .header("Authorization", "Bearer token")
136                .path("/api/v4/users/me/status/custom")
137                .json_body(serde_json::json!({"emoji":"emoji","text":"text"}
138                ));
139            resp_with
140                .status(200)
141                .header("content-type", "text/html")
142                .body("ok");
143        });
144
145        // Send an HTTP request to the mock server. This simulates your code.
146        let mut session: Box<dyn BaseSession> =
147            Box::new(Session::new(&server.url("")).with_token("token"));
148        let resp = mmstatus.send(&mut session)?;
149
150        // Ensure the specified mock was called exactly one time (or fail with a detailed error description).
151        server_mock.assert();
152        // Ensure the mock server did respond as specified.
153        assert_eq!(resp.status(), 200);
154        Ok(())
155    }
156    #[test]
157    fn catch_api_error() -> Result<()> {
158        // Start a lightweight mock server.
159        let server = MockServer::start();
160        let mut mmstatus = MMStatus::new("text".into(), "emoji".into());
161
162        // Create a mock on the server.
163        let server_mock = server.mock(|expect, resp_with| {
164            expect
165                .method(PUT)
166                .header("Authorization", "Bearer token")
167                .path("/api/v4/users/me/status/custom")
168                .json_body(serde_json::json!({"emoji":"emoji","text":"text"}
169                ));
170            resp_with
171                .status(500)
172                .header("content-type", "text/html")
173                .body("Internal error");
174        });
175
176        // Send an HTTP request to the mock server. This simulates your code.
177        let mut session: Box<dyn BaseSession> =
178            Box::new(Session::new(&server.url("")).with_token("token"));
179        let resp = mmstatus.send(&mut session);
180        assert!(resp.is_err());
181
182        // Ensure the specified mock was called exactly one time (or fail with a detailed error description).
183        server_mock.assert();
184        Ok(())
185    }
186}