Skip to main content

larkrs_client/bot/
chat.rs

1#![allow(dead_code)]
2
3use crate::LarkApiResponse;
4use crate::auth::FeishuTokenManager;
5use anyhow::{Result, anyhow};
6use reqwest::Client;
7use serde_json::Value;
8use thiserror::Error;
9
10use super::{ChatInfoItem, ChatListResponse, SendMessageRequest};
11
12#[derive(Error, Debug)]
13pub enum ChatApiError {
14    #[error("Network error: {0}")]
15    NetworkError(#[from] reqwest::Error),
16
17    #[error("JSON serialization error: {0}")]
18    SerdeError(#[from] serde_json::Error),
19
20    #[error("API error: {message} (code: {code})")]
21    ApiError { code: i32, message: String },
22}
23
24pub struct ChatClient {
25    token_manager: FeishuTokenManager,
26}
27
28impl ChatClient {
29    pub fn new() -> Self {
30        Self {
31            token_manager: FeishuTokenManager::new(),
32        }
33    }
34
35    /// Send a message to a chat
36    ///
37    /// See: https://open.feishu.cn/document/server-docs/im-v1/message/create
38    pub async fn send_message(&self, request: SendMessageRequest) -> Result<Value> {
39        let token = self.token_manager.get_token().await?;
40
41        let url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id";
42
43        let resp = Client::new()
44            .post(url)
45            .header("Authorization", format!("Bearer {}", token))
46            .header("Content-Type", "application/json")
47            .json(&request)
48            .send()
49            .await
50            .map_err(|e| anyhow!(e).context("Failed to send request for sending message"))?
51            .json::<LarkApiResponse<Value>>()
52            .await
53            .map_err(|e| anyhow!(e).context("Failed to parse send message response"))?;
54
55        match resp.is_success() {
56            true => Ok(resp.data),
57            false => Err(anyhow!(ChatApiError::ApiError {
58                code: resp.code,
59                message: resp.msg.clone(),
60            })
61            .context(format!("API returned error code: {}", resp.code))),
62        }
63    }
64
65    pub async fn send_text_message(&self, chat_id: &str, text: &str) -> Result<Value> {
66        let request = SendMessageRequest::text(chat_id, text);
67        self.send_message(request).await
68    }
69
70    /// Send a markdown message to a chat
71    ///
72    /// See: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/im-v1/message/create_json#45e0953e
73    pub async fn send_markdown_message(
74        &self,
75        chat_id: &str,
76        title: &str,
77        content: &str,
78    ) -> Result<Value> {
79        // Create a simple markdown element with the content
80        let elements = vec![vec![super::MarkdownElement {
81            tag: "md".to_string(),
82            text: content.to_string(),
83            style: None,
84        }]];
85
86        let request = SendMessageRequest::markdown(chat_id, title, elements);
87        self.send_message(request).await
88    }
89
90    /// Get a list of chats
91    ///
92    /// See: https://open.feishu.cn/document/server-docs/im-v1/chat/list
93    pub async fn get_chat_group_list(&self) -> Result<Vec<ChatInfoItem>> {
94        let token = self.token_manager.get_token().await?;
95
96        // Using reqwest's built-in query parameter handling
97        let resp = Client::new()
98            .get("https://open.feishu.cn/open-apis/im/v1/chats")
99            .query(&[("page_size", "20"), ("sort_type", "ByCreateTimeAsc")])
100            .header("Authorization", format!("Bearer {}", token))
101            .header("Content-Type", "application/json; charset=utf-8")
102            .send()
103            .await
104            .map_err(|e| anyhow!(e).context("Failed to send request for getting chat list"))?
105            .json::<LarkApiResponse<ChatListResponse>>()
106            .await
107            .map_err(|e| anyhow!(e).context("Failed to parse chat list response"))?;
108
109        match resp.is_success() {
110            true => Ok(resp.data.into()),
111            false => Err(anyhow!(ChatApiError::ApiError {
112                code: resp.code,
113                message: resp.msg.clone(),
114            })
115            .context(format!("API returned error code: {}", resp.code))),
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[tokio::test]
125    async fn test_get_chat_group_list() {
126        dotenvy::dotenv().ok();
127
128        let client = ChatClient::new();
129        let result = client.get_chat_group_list().await;
130
131        println!("Result: {:?}", result);
132    }
133
134    #[tokio::test]
135    async fn test_send_text_message() {
136        dotenvy::dotenv().ok();
137
138        let client = ChatClient::new();
139        let chat_id = std::env::var("CHAT_ID").unwrap();
140        let result = client
141            .send_text_message(&chat_id, "Test message from Rust API")
142            .await;
143
144        println!("Send message result: {:?}", result);
145    }
146
147    #[tokio::test]
148    async fn test_send_markdown_message() {
149        dotenvy::dotenv().ok();
150
151        let client = ChatClient::new();
152        let chat_id = std::env::var("CHAT_ID").unwrap();
153
154        let markdown_content = "# 股票市场实时数据\n\n**今日热门股票列表**\n\n- **阿里巴巴 (BABA)**: ¥78.45 📈 +2.3%\n- **腾讯控股 (0700.HK)**: ¥321.80 📉 -1.5%\n- **美团 (3690.HK)**: ¥125.60 📈 +3.7%\n- **京东 (JD)**: ¥142.30 📈 +0.8%\n- **百度 (BIDU)**: ¥112.75 📉 -2.1%\n- **小米集团 (1810.HK)**: ¥12.86 📈 +4.2%\n- **拼多多 (PDD)**: ¥89.35 📈 +5.6%\n\n> 数据更新时间: 2025-04-01 19:30:00";
155
156        let result = client
157            .send_markdown_message(&chat_id, "我是一个标题", markdown_content)
158            .await;
159
160        println!("Send markdown message result: {:?}", result);
161    }
162}