Skip to main content

lark_channel/message/
mod.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::Value;
3
4use crate::card::CardId;
5use crate::media::ResourceDescriptor;
6
7mod post;
8mod sender;
9mod streaming;
10
11pub use post::{PostContent, PostContentBuilder, PostDocument, PostElement, PostStyle};
12pub use sender::{MessageBuilder, MessageReplyBuilder, MessageSender, MessageSenderOptions};
13pub use streaming::{
14    ContinuingMarkdownStream, MarkdownStream, MarkdownStreamBuilder, MarkdownStreamPage,
15    ThrottledMarkdownStream,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct MessageId(pub String);
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(tag = "type", content = "id", rename_all = "snake_case")]
23pub enum Recipient {
24    Chat(String),
25    User(String),
26}
27
28/// Outbound message content supported by the Channel SDK.
29///
30/// `Card` preserves raw official `interactive` content, including template
31/// cards. Use [`MessageSender::card_message`] or [`MessageSender::card_reply`]
32/// when a validated CardKit 2.0 [`crate::Card`] is preferred.
33///
34/// This enum is non-exhaustive because future releases may add content types.
35/// Its serde representation may add matching variants as well, so older readers
36/// are not guaranteed to deserialize data written by newer releases.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(tag = "type", rename_all = "snake_case")]
39#[non_exhaustive]
40pub enum MessageContent {
41    Text { text: String },
42    Post { post: PostContent },
43    Card { card: Value },
44    CardReference { card_id: CardId },
45    Custom { msg_type: String, content: Value },
46}
47
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum MessageChatType {
51    P2p,
52    Group,
53    #[default]
54    Unknown,
55}
56
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum MessageSenderType {
60    User,
61    Bot,
62    #[default]
63    Unknown,
64}
65
66#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
67pub struct MessageSenderInfo {
68    #[serde(default)]
69    pub open_id: String,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub user_id: Option<String>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub union_id: Option<String>,
74    #[serde(default)]
75    pub sender_type: MessageSenderType,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct MessageMention {
80    #[serde(default)]
81    pub key: String,
82    #[serde(default)]
83    pub open_id: String,
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub user_id: Option<String>,
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub union_id: Option<String>,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub name: Option<String>,
90    #[serde(default)]
91    pub mentioned_type: MessageSenderType,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct NormalizedMessage {
96    pub message_id: String,
97    pub chat_id: String,
98    #[serde(default)]
99    pub chat_type: MessageChatType,
100    pub sender_id: String,
101    #[serde(default)]
102    pub sender: MessageSenderInfo,
103    #[serde(default)]
104    pub message_type: String,
105    #[serde(default)]
106    pub text: String,
107    #[serde(default, skip_serializing_if = "String::is_empty")]
108    pub raw_content: String,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub content: Option<Value>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub root_id: Option<String>,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub parent_id: Option<String>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub thread_id: Option<String>,
117    #[serde(default, deserialize_with = "deserialize_mentions")]
118    pub mentions: Vec<MessageMention>,
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub resources: Vec<ResourceDescriptor>,
121    #[serde(default)]
122    pub raw: Value,
123}
124
125impl NormalizedMessage {
126    pub fn mentions_bot(&self, bot_open_id: &str) -> bool {
127        if bot_open_id.is_empty() {
128            return false;
129        }
130
131        self.mentions
132            .iter()
133            .any(|mention| mention.open_id == bot_open_id)
134    }
135}
136
137fn deserialize_mentions<'de, D>(deserializer: D) -> Result<Vec<MessageMention>, D::Error>
138where
139    D: Deserializer<'de>,
140{
141    #[derive(Deserialize)]
142    #[serde(untagged)]
143    enum MentionValue {
144        Structured(MessageMention),
145        LegacyOpenId(String),
146    }
147
148    Vec::<MentionValue>::deserialize(deserializer).map(|mentions| {
149        mentions
150            .into_iter()
151            .map(|mention| match mention {
152                MentionValue::Structured(mention) => mention,
153                MentionValue::LegacyOpenId(open_id) => MessageMention {
154                    key: String::new(),
155                    open_id,
156                    user_id: None,
157                    union_id: None,
158                    name: None,
159                    mentioned_type: MessageSenderType::Unknown,
160                },
161            })
162            .collect()
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use serde_json::json;
169
170    use super::*;
171
172    #[test]
173    fn deserializes_older_normalized_message_json_with_defaults() {
174        let value = json!({
175            "message_id": "om_1",
176            "chat_id": "oc_1",
177            "sender_id": "ou_sender",
178            "text": "hello",
179            "mentions": ["ou_bot"]
180        });
181
182        let message: NormalizedMessage = serde_json::from_value(value).expect("message");
183
184        assert_eq!(message.chat_type, MessageChatType::Unknown);
185        assert_eq!(message.sender, MessageSenderInfo::default());
186        assert_eq!(message.message_type, "");
187        assert_eq!(message.raw_content, "");
188        assert_eq!(message.content, None);
189        assert_eq!(message.mentions.len(), 1);
190        assert!(message.resources.is_empty());
191        assert_eq!(message.mentions[0].open_id, "ou_bot");
192        assert_eq!(
193            message.mentions[0].mentioned_type,
194            MessageSenderType::Unknown
195        );
196        assert!(message.mentions_bot("ou_bot"));
197    }
198
199    #[test]
200    fn serializes_absent_mention_name_without_null_field() {
201        let mention = MessageMention {
202            key: "@_user_1".to_owned(),
203            open_id: "ou_bot".to_owned(),
204            user_id: None,
205            union_id: None,
206            name: None,
207            mentioned_type: MessageSenderType::Unknown,
208        };
209
210        let value = serde_json::to_value(mention).expect("mention");
211
212        assert_eq!(value["open_id"], "ou_bot");
213        assert!(value.get("user_id").is_none());
214        assert!(value.get("union_id").is_none());
215        assert!(value.get("name").is_none());
216    }
217
218    #[test]
219    fn mentions_bot_ignores_empty_bot_open_id() {
220        let message = NormalizedMessage {
221            message_id: "om_1".to_owned(),
222            chat_id: "oc_1".to_owned(),
223            chat_type: MessageChatType::Group,
224            sender_id: "ou_sender".to_owned(),
225            sender: MessageSenderInfo::default(),
226            message_type: "text".to_owned(),
227            text: "hello".to_owned(),
228            raw_content: String::new(),
229            content: None,
230            root_id: None,
231            parent_id: None,
232            thread_id: None,
233            mentions: vec![MessageMention {
234                key: String::new(),
235                open_id: String::new(),
236                user_id: None,
237                union_id: None,
238                name: None,
239                mentioned_type: MessageSenderType::Unknown,
240            }],
241            resources: Vec::new(),
242            raw: Value::Null,
243        };
244
245        assert!(!message.mentions_bot(""));
246    }
247}