lark-channel 0.4.0

Lark/Feishu Channel SDK for Rust
Documentation
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;

use crate::media::ResourceDescriptor;

mod sender;

pub use sender::{MessageBuilder, MessageReplyBuilder, MessageSender, MessageSenderOptions};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "id", rename_all = "snake_case")]
pub enum Recipient {
    Chat(String),
    User(String),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageContent {
    Text { text: String },
    Card { card: Value },
    Custom { msg_type: String, content: Value },
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageChatType {
    P2p,
    Group,
    #[default]
    Unknown,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageSenderType {
    User,
    Bot,
    #[default]
    Unknown,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageSenderInfo {
    #[serde(default)]
    pub open_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub user_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub union_id: Option<String>,
    #[serde(default)]
    pub sender_type: MessageSenderType,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageMention {
    #[serde(default)]
    pub key: String,
    #[serde(default)]
    pub open_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub user_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub union_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default)]
    pub mentioned_type: MessageSenderType,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NormalizedMessage {
    pub message_id: String,
    pub chat_id: String,
    #[serde(default)]
    pub chat_type: MessageChatType,
    pub sender_id: String,
    #[serde(default)]
    pub sender: MessageSenderInfo,
    #[serde(default)]
    pub message_type: String,
    #[serde(default)]
    pub text: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub raw_content: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub content: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<String>,
    #[serde(default, deserialize_with = "deserialize_mentions")]
    pub mentions: Vec<MessageMention>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub resources: Vec<ResourceDescriptor>,
    #[serde(default)]
    pub raw: Value,
}

impl NormalizedMessage {
    pub fn mentions_bot(&self, bot_open_id: &str) -> bool {
        if bot_open_id.is_empty() {
            return false;
        }

        self.mentions
            .iter()
            .any(|mention| mention.open_id == bot_open_id)
    }
}

fn deserialize_mentions<'de, D>(deserializer: D) -> Result<Vec<MessageMention>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum MentionValue {
        Structured(MessageMention),
        LegacyOpenId(String),
    }

    Vec::<MentionValue>::deserialize(deserializer).map(|mentions| {
        mentions
            .into_iter()
            .map(|mention| match mention {
                MentionValue::Structured(mention) => mention,
                MentionValue::LegacyOpenId(open_id) => MessageMention {
                    key: String::new(),
                    open_id,
                    user_id: None,
                    union_id: None,
                    name: None,
                    mentioned_type: MessageSenderType::Unknown,
                },
            })
            .collect()
    })
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn deserializes_older_normalized_message_json_with_defaults() {
        let value = json!({
            "message_id": "om_1",
            "chat_id": "oc_1",
            "sender_id": "ou_sender",
            "text": "hello",
            "mentions": ["ou_bot"]
        });

        let message: NormalizedMessage = serde_json::from_value(value).expect("message");

        assert_eq!(message.chat_type, MessageChatType::Unknown);
        assert_eq!(message.sender, MessageSenderInfo::default());
        assert_eq!(message.message_type, "");
        assert_eq!(message.raw_content, "");
        assert_eq!(message.content, None);
        assert_eq!(message.mentions.len(), 1);
        assert!(message.resources.is_empty());
        assert_eq!(message.mentions[0].open_id, "ou_bot");
        assert_eq!(
            message.mentions[0].mentioned_type,
            MessageSenderType::Unknown
        );
        assert!(message.mentions_bot("ou_bot"));
    }

    #[test]
    fn serializes_absent_mention_name_without_null_field() {
        let mention = MessageMention {
            key: "@_user_1".to_owned(),
            open_id: "ou_bot".to_owned(),
            user_id: None,
            union_id: None,
            name: None,
            mentioned_type: MessageSenderType::Unknown,
        };

        let value = serde_json::to_value(mention).expect("mention");

        assert_eq!(value["open_id"], "ou_bot");
        assert!(value.get("user_id").is_none());
        assert!(value.get("union_id").is_none());
        assert!(value.get("name").is_none());
    }

    #[test]
    fn mentions_bot_ignores_empty_bot_open_id() {
        let message = NormalizedMessage {
            message_id: "om_1".to_owned(),
            chat_id: "oc_1".to_owned(),
            chat_type: MessageChatType::Group,
            sender_id: "ou_sender".to_owned(),
            sender: MessageSenderInfo::default(),
            message_type: "text".to_owned(),
            text: "hello".to_owned(),
            raw_content: String::new(),
            content: None,
            root_id: None,
            parent_id: None,
            thread_id: None,
            mentions: vec![MessageMention {
                key: String::new(),
                open_id: String::new(),
                user_id: None,
                union_id: None,
                name: None,
                mentioned_type: MessageSenderType::Unknown,
            }],
            resources: Vec::new(),
            raw: Value::Null,
        };

        assert!(!message.mentions_bot(""));
    }
}