summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaweł Dybiec <pawel@dybiec.info>2022-12-26 20:26:37 +0000
committerPaweł Dybiec <pawel@dybiec.info>2022-12-26 22:34:24 +0000
commit73dbbabb69278f7784251737b28c4d2f19f93b58 (patch)
treeffe1a63ff8c7e5bd49298d75c0a3389fc68152f1
parentAdd dockerfile and build/publish step for it (diff)
Add basic model for messages + parsing
-rw-r--r--src/main.rs45
-rw-r--r--src/mattermost/client.rs (renamed from src/mattermost.rs)45
-rw-r--r--src/mattermost/mod.rs4
-rw-r--r--src/mattermost/model.rs299
4 files changed, 331 insertions, 62 deletions
diff --git a/src/main.rs b/src/main.rs
index 36dfbb8..b735100 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,54 +1,29 @@
 mod mattermost;
-
 use anyhow::anyhow;
 use tokio::{self};
-use tracing::{debug, info};
+use tracing::debug;
 
 struct Vav {}
 #[async_trait::async_trait]
 impl mattermost::Handler for Vav {
     async fn handle(
         &self,
-        value: serde_json::Value,
+        update: mattermost::model::WebsocketUpdate,
         client: &mattermost::Client,
     ) -> Result<(), anyhow::Error> {
-        debug!("{value:?}");
-        let object = value
-            .as_object()
-            .ok_or(anyhow!("Response from mattermost should be object"))?;
-        if object
-            .get("event")
-            .ok_or(anyhow!("missing event type"))?
-            .as_str()
-            .ok_or(anyhow!("event type should be a string"))?
-            != "posted"
-        {
-            return Ok(());
-        }
-        let post: serde_json::Value = serde_json::from_str(
-            object
-                .get("data")
-                .unwrap()
-                .get("post")
-                .ok_or(anyhow!("missing post field"))?
-                .as_str()
-                .ok_or(anyhow!("post should be string"))?,
-        )?;
-        debug!("Post field {post:?}");
-        let object = post
-            .as_object()
-            .ok_or(anyhow!("Post field should represent legit object"))?;
-        let message = object
-            .get("message")
-            .ok_or(anyhow!("missing message"))?
-            .as_str()
-            .ok_or(anyhow!("message should be string"))?;
+        debug!("as json: {update:?}");
+
+        let posted = match update.update {
+            mattermost::model::Update::Posted(posted) => posted,
+            _ => return Ok(()),
+        };
+        let message = &posted.post.message;
         if !message.starts_with('!') {
             return Ok(());
         }
         let message = message.strip_prefix('!').unwrap();
         match message{
-            "adres" => client.reply_to(&post, "Uniwersytet Wrocławski Instytut Informatyki; pl. Frederyka Joliot-Curie 15; 50-383 Wrocław".to_owned()).await?,
+            "adres" => client.reply_to(posted.post, "Uniwersytet Wrocławski Instytut Informatyki; pl. Frederyka Joliot-Curie 15; 50-383 Wrocław".to_owned()).await?,
             _ => return Err(anyhow!("Unrecognized command {message}")),
         }
         Ok(())
diff --git a/src/mattermost.rs b/src/mattermost/client.rs
index c8759c3..abdf1ba 100644
--- a/src/mattermost.rs
+++ b/src/mattermost/client.rs
@@ -3,6 +3,8 @@ use async_tungstenite::{tokio::connect_async, tungstenite::Message};
 use futures_util::{SinkExt, StreamExt};
 use serde_json::json;
 use tracing::{debug, warn};
+
+use super::WebsocketMessage;
 pub struct AuthData {
     pub(crate) login: String,
     pub(crate) password: String,
@@ -18,7 +20,7 @@ pub struct Client {
 pub trait Handler {
     async fn handle(
         &self,
-        message: serde_json::Value,
+        message: super::model::WebsocketUpdate,
         client: &Client,
     ) -> Result<(), anyhow::Error>;
 }
@@ -72,15 +74,18 @@ impl Client {
             match message {
                 Ok(message) => match message {
                     Message::Text(text) => {
-                        let json: Result<serde_json::Value, _> = serde_json::from_str(&text);
+                        let json: Result<WebsocketMessage, _> = serde_json::from_str(&text);
                         match json {
-                            Ok(json) => {
-                                // debug!("Got json: {json}");
-                                if let Err(err) = handler.handle(json, &self).await {
-                                    warn!("Handler returned error: {err}");
-                                };
+                            Ok(websocketmessage) => {
+                                if let WebsocketMessage::Update(update) = websocketmessage {
+                                    if let Err(err) = handler.handle(update, &self).await {
+                                        warn!("Handler returned error: {err}");
+                                    }
+                                } else {
+                                    debug!("Got only websocket reply. {websocketmessage:?}");
+                                }
                             }
-                            Err(err) => warn!("Error while deserializing: {err}"),
+                            Err(err) => warn!("Error while deserializing: {err} {text}"),
                         }
                     }
                     Message::Close(_) => break,
@@ -93,34 +98,20 @@ impl Client {
         }
         Ok(())
     }
+    #[allow(unreachable_code, unused_variables)] //NOCOMMIT: remove
     pub(crate) async fn reply_to(
         &self,
-        post: &serde_json::Value,
+        post: super::model::Post,
         reply: String,
     ) -> Result<(), anyhow::Error> {
         let token = self
             .bearer_token
             .clone()
             .ok_or(anyhow!("Missing bearer token"))?;
-        let object = post
-            .as_object()
-            .ok_or(anyhow!("Response from mattermost should be object"))?;
-        let id = object
-            .get("id")
-            .ok_or(anyhow!("missing id"))?
-            .as_str()
-            .ok_or(anyhow!("id should be a string"))?;
-        let root_id = object
-            .get("root_id")
-            .ok_or(anyhow!("missing root_id"))?
-            .as_str()
-            .ok_or(anyhow!("root_id should be a string"))?;
+        let id = post.id;
+        let root_id = post.root_id;
         let post_id = if root_id == "" { id } else { root_id };
-        let channel_id = object
-            .get("channel_id")
-            .ok_or(anyhow!("missing channel_id"))?
-            .as_str()
-            .ok_or(anyhow!("channel_id should be a string"))?;
+        let channel_id = post.channel_id;
 
         debug!("Sending response");
         let response = self
diff --git a/src/mattermost/mod.rs b/src/mattermost/mod.rs
new file mode 100644
index 0000000..68c4465
--- /dev/null
+++ b/src/mattermost/mod.rs
@@ -0,0 +1,4 @@
+pub mod client;
+pub mod model;
+pub use client::{AuthData, Client, Handler};
+pub use model::WebsocketMessage;
diff --git a/src/mattermost/model.rs b/src/mattermost/model.rs
new file mode 100644
index 0000000..9d4d78e
--- /dev/null
+++ b/src/mattermost/model.rs
@@ -0,0 +1,299 @@
+use serde::Deserialize;
+type PostId = String;
+type UserId = String;
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+#[serde(untagged)]
+pub enum WebsocketMessage {
+    Update(WebsocketUpdate),
+    Reply(WebsocketReply),
+}
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct WebsocketReply {
+    pub status: String,
+    pub seq_reply: u32,
+}
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct Broadcast {
+    pub channel_id: String,
+    pub connection_id: String,
+    pub omit_connection_id: String,
+    // omit_users:
+    pub team_id: String,
+    pub user_id: String,
+}
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct WebsocketUpdateStuff {
+    pub broadcast: Broadcast,
+    pub seq: u32,
+}
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct WebsocketUpdate {
+    #[serde(flatten)]
+    pub stuff: WebsocketUpdateStuff,
+    #[serde(flatten)]
+    pub update: Update,
+}
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+#[serde(tag = "event", content = "data")]
+pub enum Update {
+    #[serde(rename = "posted")]
+    Posted(Posted),
+    #[serde(rename = "hello")]
+    Hello(Hello),
+    #[serde(rename = "status_change")]
+    StatusChange {},
+    #[serde(rename = "typing")]
+    Typing {},
+    #[serde(rename = "channel_viewed")]
+    ChannelViewed {},
+    #[serde(other)]
+    Other,
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct Posted {
+    pub channel_display_name: String,
+    pub channel_name: String,
+    pub channel_type: String,
+    #[serde(deserialize_with = "deserialize_json_field")]
+    pub mentions: Vec<String>,
+    #[serde(deserialize_with = "deserialize_json_field")]
+    pub post: Post,
+}
+fn deserialize_json_field<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+    D: serde::Deserializer<'de>,
+    T: serde::de::DeserializeOwned,
+{
+    let buf: String = String::deserialize(deserializer)?;
+    serde_json::from_str(&buf).map_err(|x| {
+        serde::de::Error::custom(format!("Error while deserializing json encoded field {x}"))
+    })
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct Post {
+    pub id: String,
+    pub create_at: u64,
+    pub update_at: u64,
+    pub edit_at: u64,
+    pub delete_at: u64,
+    pub is_pinned: bool,
+    pub user_id: String,
+    pub channel_id: String,
+    // pub hashtags: String,
+    // pub last_reply_at: u64,
+    pub message: String,
+    // pub metadata:,
+    // pub original_id: String,
+    // pub participants:,
+    // pub pending_post_id: String,
+    // pub preops
+    // pub reply_count: u64,
+    pub root_id: String,
+    #[serde(rename = "type")]
+    pub ttype: String,
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct Hello {
+    pub connection_id: String,
+    pub server_version: String,
+}
+#[cfg(test)]
+mod tests {
+    use serde_json::Value;
+
+    use crate::mattermost::model::{
+        Broadcast, Post, Update, WebsocketUpdate, WebsocketUpdateStuff,
+    };
+    #[test]
+    fn parse_post() {
+        let post = Post {
+            id: "a".to_owned(),
+            create_at: 1,
+            update_at: 2,
+            edit_at: 3,
+            delete_at: 4,
+            is_pinned: true,
+            user_id: "b".to_owned(),
+            channel_id: "c".to_owned(),
+            message: "d".to_owned(),
+            root_id: "e".to_owned(),
+            ttype: "f".to_owned(),
+        };
+        let post_str = r#"{
+                    "id":"a",
+                    "create_at":1,
+                    "update_at":2,
+                    "edit_at":3,
+                    "delete_at":4,
+                    "is_pinned":true,
+                    "user_id":"b",
+                    "channel_id":"c",
+                    "message":"d",
+                    "root_id":"e",
+                    "type":"f"
+                }"#;
+        let d: Result<Post, _> = serde_json::from_str(post_str);
+        assert_eq!(d.unwrap(), post);
+        let data = format!(
+            "\"{}\"",
+            post_str.replace('\n', "\\n").replace('\"', "\\\"")
+        );
+        let x: Value = serde_json::from_str(&data).unwrap();
+        let s = x.as_str().unwrap();
+        let d: Result<Post, _> = serde_json::from_str(s);
+        assert_eq!(d.unwrap(), post);
+    }
+
+    #[test]
+    fn parse_update() {
+        let post = Post {
+            id: "a".to_owned(),
+            create_at: 1,
+            update_at: 2,
+            edit_at: 3,
+            delete_at: 4,
+            is_pinned: true,
+            user_id: "b".to_owned(),
+            channel_id: "c".to_owned(),
+            message: "d".to_owned(),
+            root_id: "e".to_owned(),
+            ttype: "f".to_owned(),
+        };
+        let data = r#"
+        {
+            "data":{
+                "connection_id":  "aaaa",
+                "server_version": "2137"
+            },
+            "event":"hello"
+        }"#;
+        let update: Result<Update, _> = serde_json::from_str(data);
+        assert_eq!(
+            update.unwrap(),
+            Update::Hello(super::Hello {
+                connection_id: "aaaa".to_owned(),
+                server_version: "2137".to_owned()
+            })
+        );
+        let data = r#"
+        {
+            "data":{
+                "channel_display_name":"a",
+                "channel_name":"b",
+                "channel_type":"c",
+                "mentions":"[\"d\"]",
+                "post": "{\"id\":\"a\",\"create_at\":1,\"update_at\":2,\"edit_at\":3,\"delete_at\":4,\"is_pinned\":true,\"user_id\":\"b\",\"channel_id\":\"c\",\"message\":\"d\",\"root_id\":\"e\",\"type\":\"f\"}"
+            },
+            "event":"posted"
+        }"#;
+        let update: Result<Update, _> = serde_json::from_str(&data);
+        assert_eq!(
+            update.unwrap(),
+            Update::Posted(super::Posted {
+                channel_display_name: "a".to_owned(),
+                channel_name: "b".to_owned(),
+                channel_type: "c".to_owned(),
+                mentions: vec!["d".to_owned()],
+                post: post
+            })
+        );
+    }
+    #[test]
+    fn parse_websocketupdate() {
+        let post = Post {
+            id: "a".to_owned(),
+            create_at: 1,
+            update_at: 2,
+            edit_at: 3,
+            delete_at: 4,
+            is_pinned: true,
+            user_id: "b".to_owned(),
+            channel_id: "c".to_owned(),
+            message: "d".to_owned(),
+            root_id: "e".to_owned(),
+            ttype: "f".to_owned(),
+        };
+        let data = r#"
+        {
+            "broadcast":{
+                "channel_id":"a",
+                "connection_id":"b",
+                "omit_connection_id":"c",
+                "team_id":"d",
+                "user_id":"e"
+            },
+            "data":{
+                "connection_id":  "aaaa",
+                "server_version": "2137"
+            },
+            "event":"hello",
+            "seq":2137
+        }"#;
+        let update: Result<WebsocketUpdate, _> = serde_json::from_str(data);
+        assert_eq!(
+            update.unwrap(),
+            WebsocketUpdate {
+                stuff: WebsocketUpdateStuff {
+                    broadcast: Broadcast {
+                        channel_id: "a".to_owned(),
+                        connection_id: "b".to_owned(),
+                        omit_connection_id: "c".to_owned(),
+                        team_id: "d".to_owned(),
+                        user_id: "e".to_owned()
+                    },
+                    seq: 2137,
+                },
+                update: Update::Hello(super::Hello {
+                    connection_id: "aaaa".to_owned(),
+                    server_version: "2137".to_owned()
+                })
+            }
+        );
+        let data = r#"
+        {
+            "broadcast":{
+                "channel_id":"a",
+                "connection_id":"b",
+                "omit_connection_id":"c",
+                "team_id":"d",
+                "user_id":"e"
+            },
+            "data":{
+                "channel_display_name":"a",
+                "channel_name":"b",
+                "channel_type":"c",
+                "mentions":"[\"d\"]",
+                "post": "{\"id\":\"a\",\"create_at\":1,\"update_at\":2,\"edit_at\":3,\"delete_at\":4,\"is_pinned\":true,\"user_id\":\"b\",\"channel_id\":\"c\",\"message\":\"d\",\"root_id\":\"e\",\"type\":\"f\"}"
+            },
+            "event":"posted",
+            "seq": 2137
+        }"#;
+        let update: Result<WebsocketUpdate, _> = serde_json::from_str(&data);
+        assert_eq!(
+            update.unwrap(),
+            WebsocketUpdate {
+                stuff: WebsocketUpdateStuff {
+                    broadcast: Broadcast {
+                        channel_id: "a".to_owned(),
+                        connection_id: "b".to_owned(),
+                        omit_connection_id: "c".to_owned(),
+                        team_id: "d".to_owned(),
+                        user_id: "e".to_owned()
+                    },
+                    seq: 2137,
+                },
+                update: Update::Posted(super::Posted {
+                    channel_display_name: "a".to_owned(),
+                    channel_name: "b".to_owned(),
+                    channel_type: "c".to_owned(),
+                    mentions: vec!["d".to_owned()],
+                    post: post
+                })
+            }
+        );
+    }
+}