1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Module responsible for sending custom status change to mattermost.
use crate::utils::parse_from_hmstr;
use anyhow::Result;
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use serde_json as json;
use std::fmt;
use thiserror::Error;
use tracing::debug;

/// Implement errors specific to `MMStatus`
#[allow(missing_docs)]
#[derive(Debug, Error)]
pub enum MMSError {
    #[error("Bad json data")]
    BadJSONData(#[from] serde_json::error::Error),
    #[error("HTTP request error")]
    HTTPRequestError(#[from] ureq::Error),
}

/// Custom struct to serialize the HTTP POST data into a json objecting using serde_json
/// For a description of these fields see the [MatterMost OpenApi sources](https://github.com/mattermost/mattermost-api-reference/blob/master/v4/source/status.yaml)
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MMStatus {
    /// custom status text description
    pub text: String,
    /// custom status emoji name
    pub emoji: String,
    /// custom status duration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration: Option<String>,
    /// custom status expiration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<DateTime<Local>>,
    /// base URL of the mattermost server like https://mattermost.example.com
    #[serde(skip_serializing)]
    base_uri: String,
    /// private access token for current user on the `base_uri` mattermost instance
    #[serde(skip_serializing)]
    token: String,
}

impl fmt::Display for MMStatus {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}:{} (duration: {:?}, expire at: {:?})",
            self.emoji, self.text, self.duration, self.expires_at
        )
    }
}

impl MMStatus {
    /// Create a `MMStatus` ready to be sent to the `mm_base_uri` mattermost instance.
    /// Authentication is done with the private access `token`.
    pub fn new(text: String, emoji: String, mm_base_uri: String, token: String) -> MMStatus {
        let uri = mm_base_uri + "/api/v4/users/me/status/custom";
        MMStatus {
            text,
            emoji,
            duration: None,
            expires_at: None,
            base_uri: uri,
            token,
        }
    }
    /// Add expiration time with the format "hh:mm" to the mattermost custom status
    pub fn expires_at(mut self, time_str: &Option<String>) -> Self {
        // do not set expiry time if set in the past
        if let Some(expiry) = parse_from_hmstr(time_str) {
            if Local::now() < expiry {
                self.expires_at = Some(expiry);
                self.duration = Some("date_and_time".to_owned());
            } else {
                debug!("now {:?} >= expiry {:?}", Local::now(), expiry);
            }
        }
        // let dt: NaiveDateTime = NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11);
        self
    }
    /// This function is essentially used for debugging or testing
    pub fn to_json(&self) -> Result<String, MMSError> {
        json::to_string(&self).map_err(MMSError::BadJSONData)
    }

    /// Send the new custom status
    pub fn send(&self) -> Result<u16, MMSError> {
        debug!("Post status: {}", self.to_owned().to_json()?);
        let response = ureq::put(&self.base_uri)
            .set("Authorization", &("Bearer ".to_owned() + &self.token))
            .send_json(serde_json::to_value(&self)?)
            .map_err(MMSError::HTTPRequestError)?;
        Ok(response.status())
    }
}

#[cfg(test)]
mod should {
    use super::*;
    use httpmock::prelude::*;
    #[test]
    fn send_required_json_for_mmstatus() -> Result<()> {
        // Start a lightweight mock server.
        let server = MockServer::start();
        let mmstatus = MMStatus::new(
            "text".to_string(),
            "emoji".to_string(),
            server.url(""),
            "token".to_string(),
        );

        // Create a mock on the server.
        let server_mock = server.mock(|expect, resp_with| {
            expect
                .method(PUT)
                .header("Authorization", "Bearer token")
                .path("/api/v4/users/me/status/custom")
                .json_body(serde_json::json!({"emoji":"emoji","text":"text"}
                ));
            resp_with
                .status(200)
                .header("content-type", "text/html")
                .body("ok");
        });

        // Send an HTTP request to the mock server. This simulates your code.
        let status = mmstatus.send()?;

        // Ensure the specified mock was called exactly one time (or fail with a detailed error description).
        server_mock.assert();
        // Ensure the mock server did respond as specified.
        assert_eq!(status, 200);
        Ok(())
    }
}