zulip_rs 0.2.0

Rust library to access the Zulip API
Documentation
use crate::message::*;
use crate::rc::{parse_from_str, ZulipRuntimeConfig};
use crate::CommonMutateResponse;
use anyhow::{Context, Result};
use reqwest::{Method, RequestBuilder};

pub struct Client {
    rc: ZulipRuntimeConfig,
}

impl Client {
    pub fn new(zulip_rc: &str) -> Result<Self> {
        let rc = parse_from_str(zulip_rc)?;
        Ok(Client { rc })
    }
    pub fn parse(zulip_rc: &str) -> Result<Self> {
        let rc = parse_from_str(zulip_rc)?;
        Ok(Client { rc })
    }
    pub async fn send_message(&self, req: SendMessageRequest) -> Result<SendMessageResponse> {
        let result = self
            .http_client(Method::POST, "/api/v1/messages")
            .form(&req)
            .send()
            .await?;
        let resp: SendMessageResponse = result.json().await?;
        Ok(resp)
    }
    pub async fn get_messages(&self, req: GetMessagesRequest) -> Result<GetMessagesResponse> {
        let qs = serde_qs::to_string(&req)?;
        let narrow = serde_json::to_string(&req.narrow)?;
        let qs = format!("/api/v1/messages?{}&narrow={}", qs, narrow);
        println!("{}", qs);
        let resp: GetMessagesResponse = self
            .http_client(Method::GET, &qs)
            .send()
            .await
            .context("get messages send failed")?
            .json()
            .await
            .context("deserialize to GetMessagesResponse failed")?;
        Ok(resp)
    }
    pub async fn delete_message(&self, id: i64) -> Result<DeleteMessageResponse> {
        let resp: DeleteMessageResponse = self
            .http_client(Method::DELETE, &format!("/api/v1/messages/{}", id))
            .send()
            .await
            .context("delete message failed")?
            .json()
            .await
            .context("deserialize to DeleteMessageResponse failed")?;
        Ok(resp)
    }
    pub async fn edit_message(&self, req: EditMessageRequest) -> Result<CommonMutateResponse> {
        let resp: CommonMutateResponse = self
            .http_client(
                Method::PATCH,
                &format!("/api/v1/messages/{}", req.message_id),
            )
            .form(&req)
            .send()
            .await
            .context("delete message failed")?
            .json()
            .await
            .context("deserialize to DeleteMessageResponse failed")?;
        Ok(resp)
    }
    pub async fn add_emoji_reaction(
        &self,
        req: AddEmojiReactionRequest,
    ) -> Result<CommonMutateResponse> {
        let resp: CommonMutateResponse = self
            .http_client(
                Method::POST,
                &format!("/api/v1/messages/{}/reactions", req.message_id),
            )
            .form(&req)
            .send()
            .await
            .context("delete message failed")?
            .json()
            .await
            .context("deserialize to DeleteMessageResponse failed")?;
        Ok(resp)
    }
    pub async fn remove_emoji_reaction(
        &self,
        req: RemoveEmojiReactionRequest,
    ) -> Result<CommonMutateResponse> {
        let resp: CommonMutateResponse = self
            .http_client(
                Method::DELETE,
                &format!("/api/v1/messages/{}/reactions", req.message_id),
            )
            .form(&req)
            .send()
            .await
            .context("delete message failed")?
            .json()
            .await
            .context("deserialize to DeleteMessageResponse failed")?;
        Ok(resp)
    }
    fn http_client(&self, method: Method, endpoint: &str) -> RequestBuilder {
        let client = reqwest::Client::new();
        let url = format!("{}{}", &self.rc.api.site, endpoint);
        client
            .request(method, url)
            .basic_auth(&self.rc.api.email, Some(&self.rc.api.key))
            .header("application", "x-www-form-urlencoded")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use httpmock::{
        Method::{DELETE, GET, POST},
        MockServer,
    };
    use std::net::SocketAddr;

    #[tokio::test]
    async fn test_send_private_message() {
        let server = MockServer::start();
        let mock = server.mock(|when, then| {
            when.method(POST).path("/api/v1/messages");
            then.status(200)
                .body(r#"{"result": "success", "msg": "", "id": 123}"#);
        });
        let client = Client::new(&rc(server.address())).unwrap();
        let req = SendMessageRequest::Private {
            to: "[8]".to_string(),
            content: "abc".to_string(),
        };
        let result = client.send_message(req).await;
        mock.assert();
        assert!(result.is_ok());
    }
    #[tokio::test]
    async fn test_send_stream_message() {
        let server = MockServer::start();
        let mock = server.mock(|when, then| {
            when.method(POST).path("/api/v1/messages");
            then.status(200)
                .body(r#"{"result": "success", "msg": "", "id": 123}"#);
        });
        let client = Client::new(&rc(server.address())).unwrap();
        let req = SendMessageRequest::Stream {
            to: "[8]".to_string(),
            topic: "test".to_string(),
            content: "abc".to_string(),
        };
        let result = client.send_message(req).await;
        mock.assert();
        assert!(result.is_ok());
    }
    #[tokio::test]
    async fn test_get_messages() {
        let server = MockServer::start();
        let mock = server.mock(|when, then| {
            when.method(GET).path("/api/v1/messages");
            then.status(200).body(message_template());
        });
        let client = Client::new(&rc(server.address())).unwrap();

        let req = GetMessagesRequest::new(0, 0);

        let result = client.get_messages(req).await;
        mock.assert();
        assert!(result.is_ok());
    }
    #[tokio::test]
    async fn test_delete_messages() {
        let server = MockServer::start();
        let id = 123;
        let mock = server.mock(|when, then| {
            when.method(DELETE).path(format!("/api/v1/messages/{}", id));
            then.status(200).body(r#"{"result": "success", "msg": ""}"#);
        });
        let client = Client::new(&rc(server.address())).unwrap();
        let result = client.delete_message(id).await;
        mock.assert();
        assert!(result.is_ok());
    }
    fn rc(addr: &SocketAddr) -> String {
        format!(
            "[api]\nemail=test@test.tester.com\nkey=testkey\nsite=http://{}\n",
            addr
        )
    }
    fn message_template() -> String {
        r#"{
    "anchor": 21,
    "found_anchor": true,
    "found_newest": true,
    "messages": [
        {
            "avatar_url": "https://secure.gravatar.com/avatar/6d8cad0fd00256e7b40691d27ddfd466?d=identicon&version=1",
            "client": "populate_db",
            "content": "<p>Security experts agree that relational algorithms are an interesting new topic in the field of networking, and scholars concur.</p>",
            "content_type": "text/html",
            "display_recipient": [
                {
                    "email": "hamlet@zulip.com",
                    "full_name": "King Hamlet",
                    "id": 4,
                    "is_mirror_dummy": false
                },
                {
                    "email": "iago@zulip.com",
                    "full_name": "Iago",
                    "id": 5,
                    "is_mirror_dummy": false
                },
                {
                    "email": "prospero@zulip.com",
                    "full_name": "Prospero from The Tempest",
                    "id": 8,
                    "is_mirror_dummy": false
                }
            ],
            "flags": [
                "read"
            ],
            "id": 16,
            "is_me_message": false,
            "reactions": [],
            "recipient_id": 27,
            "sender_email": "hamlet@zulip.com",
            "sender_full_name": "King Hamlet",
            "sender_id": 4,
            "sender_realm_str": "zulip",
            "subject": "",
            "submessages": [],
            "timestamp": 1527921326,
            "topic_links": [],
            "type": "private"
        },
        {
            "avatar_url": "https://secure.gravatar.com/avatar/6d8cad0fd00256e7b40691d27ddfd466?d=identicon&version=1",
            "client": "populate_db",
            "content": "<p>Wait, is this from the frontend js code or backend python code</p>",
            "content_type": "text/html",
            "display_recipient": "Verona",
            "flags": [
                "read"
            ],
            "id": 21,
            "is_me_message": false,
            "reactions": [],
            "recipient_id": 20,
            "sender_email": "hamlet@zulip.com",
            "sender_full_name": "King Hamlet",
            "sender_id": 4,
            "sender_realm_str": "zulip",
            "stream_id": 5,
            "subject": "Verona3",
            "submessages": [],
            "timestamp": 1527939746,
            "topic_links": [],
            "type": "stream"
        }
    ],
    "msg": "",
    "result": "success"
}"#.to_string()
    }
}