Skip to main content

bpi_rs/message/
private_msg.rs

1use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
2use serde::{ Deserialize, Serialize };
3use serde_json::{ Value, json };
4
5use chrono::Utc;
6use uuid::Uuid;
7
8// --- API 结构体 ---
9
10/// 未读私信数数据
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct SingleUnreadData {
13    pub unfollow_unread: u32,
14    pub follow_unread: u32,
15    pub unfollow_push_msg: u32,
16    pub dustbin_push_msg: u32,
17    pub dustbin_unread: u32,
18    pub biz_msg_unfollow_unread: u32,
19    pub biz_msg_follow_unread: u32,
20    pub custom_unread: u32,
21}
22
23/// 发送私信的响应数据
24#[derive(Debug, Clone, Deserialize, Serialize)]
25pub struct SendMsgData {
26    pub msg_key: Option<u64>,
27    pub e_infos: Option<Vec<EmojiInfo>>,
28    pub msg_content: Option<String>,
29    pub key_hit_infos: Option<KeyHitInfos>,
30}
31
32/// 表情信息
33#[derive(Debug, Clone, Deserialize, Serialize)]
34pub struct EmojiInfo {
35    pub text: String,
36    pub uri: String,
37    pub size: u32,
38    pub gif_url: Option<String>,
39}
40
41/// 触发的提示信息
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct KeyHitInfos {
44    pub toast: Option<String>,
45    pub rule_id: Option<u64>,
46    pub high_text: Option<Vec<Value>>, // 具体结构待补充
47}
48
49/// 发送的图片格式
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct Image {
52    pub url: String,
53    pub height: u64,
54    pub width: u64,
55    #[serde(rename = "imageType")]
56    pub image_type: Option<String>,
57    pub original: Option<u64>, // 1 代表是原图
58    pub size: f64,
59}
60
61/// 私信消息类型
62pub enum MessageType {
63    /// 文本消息,内容为纯文本
64    Text(String),
65    /// 图片消息,内容为JSON字符串
66    Image(Image),
67}
68
69impl BpiClient {
70    /// 获取未读私信数。
71    ///
72    /// # 文档
73    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/message)
74    ///
75    /// # 参数
76    ///
77    /// | 名称 | 类型 | 说明 |
78    /// | ---- | ---- | ---- |
79    /// | `unread_type` | `Option<u32>` | 未读类型(默认 All) |
80    /// | `show_unfollow_list` | `Option<u32>` | 是否返回未关注推送消息数 |
81    /// | `show_dustbin` | `Option<u32>` | 是否返回被拦截私信数 |
82    ///
83    /// 备注:若 `unread_type` 为 Blocked,`show_dustbin` 必须为 true。
84    pub async fn message_single_unread(
85        &self,
86        unread_type: Option<u32>,
87        show_unfollow_list: Option<u32>,
88        show_dustbin: Option<u32>
89    ) -> Result<BpiResponse<SingleUnreadData>, BpiError> {
90        let params = [
91            ("build", "0"),
92            ("mobi_app", "web"),
93            ("unread_type", &unread_type.map_or("0".to_string(), |v| v.to_string())),
94            ("show_unfollow_list", if show_unfollow_list == Some(1) { "1" } else { "0" }),
95            ("show_dustbin", if show_dustbin.is_some() { "1" } else { "0" }),
96        ];
97
98        self
99            .get("https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread")
100            .query(&params)
101            .send_bpi("获取未读私信数").await
102    }
103
104    /// 发送私信。
105    ///
106    /// # 文档
107    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/message)
108    ///
109    /// # 参数
110    ///
111    /// | 名称 | 类型 | 说明 |
112    /// | ---- | ---- | ---- |
113    /// | `receiver_id` | u64 | 接收者 ID |
114    /// | `receiver_type` | u32 | 接收者类型:1 用户,2 粉丝团 |
115    /// | `message_type` | MessageType | 消息类型(文本/图片) |
116    pub async fn message_send(
117        &self,
118        receiver_id: u64,
119        receiver_type: u32,
120        message_type: MessageType
121    ) -> Result<BpiResponse<SendMsgData>, BpiError> {
122        // 1. 获取必需的参数
123        let csrf = self.csrf()?;
124        let sender_uid = &self.get_account().ok_or(BpiError::auth("未登录"))?.dede_user_id;
125        let dev_id = Uuid::new_v4().to_string();
126        let timestamp = Utc::now().timestamp();
127
128        let msg_type = match message_type {
129            MessageType::Text(_) => 1,
130            MessageType::Image(_) => 2,
131        };
132
133        // 2. 准备请求体参数
134        let mut form = vec![
135            ("msg[sender_uid]", sender_uid.to_string()),
136            ("msg[receiver_id]", receiver_id.to_string()),
137            ("msg[receiver_type]", receiver_type.to_string()),
138            ("msg[msg_type]", msg_type.to_string()),
139            ("msg[msg_status]", "0".to_string()),
140            ("msg[dev_id]", dev_id.clone()),
141            ("msg[timestamp]", timestamp.to_string()),
142            ("msg[new_face_version]", "1".to_string()),
143            ("csrf", csrf.clone()),
144            ("csrf_token", csrf.clone()),
145            ("build", "0".to_string()),
146            ("mobi_app", "web".to_string())
147        ];
148
149        // 3. 构造 msg[content] 参数
150        let content = match message_type {
151            MessageType::Text(text) => json!({ "content": text }).to_string(),
152            MessageType::Image(image) => serde_json::to_string(&image)?,
153        };
154
155        form.push(("msg[content]", content));
156
157        let params = vec![
158            ("w_sender_uid", sender_uid.to_string()),
159            ("w_receiver_id", receiver_id.to_string()),
160            ("w_dev_id", dev_id.clone())
161        ];
162
163        let signed_params = self.get_wbi_sign2(params).await?;
164
165        // 发送请求
166        self
167            .post("https://api.vc.bilibili.com/web_im/v1/web_im/send_msg")
168            .query(&signed_params)
169            .form(&form)
170            .send_bpi("发送私信").await
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use std::path::Path;
178    use tracing::info;
179
180    #[tokio::test]
181    async fn test_get_single_unread() -> Result<(), BpiError> {
182        let bpi = BpiClient::new();
183
184        // 默认查询所有未读私信数
185        let all_unread_resp = bpi.message_single_unread(None, None, None).await?;
186        let all_unread_data = all_unread_resp.into_data()?;
187        println!("所有未读私信数: {:?}", all_unread_data);
188
189        assert_eq!(all_unread_data.dustbin_unread, 0); // show_dustbin为false时,该值为0
190
191        Ok(())
192    }
193
194    #[tokio::test]
195    async fn test_send_text_message() -> Result<(), BpiError> {
196        let bpi = BpiClient::new();
197        let receiver_id = 107997089; // 替换为你要发送消息的目标用户mid
198        // let message_content = "这是一个测试消息。";
199        //
200        // let resp = bpi
201        //     .send_message(
202        //         receiver_id,
203        //         1, // 接收者类型:用户
204        //         MessageType::Text(message_content),
205        //     )
206        //     .await?;
207
208        let test_file = Path::new("./assets/test.jpg");
209        if !test_file.exists() {
210            return Err(BpiError::parse("Test file 'test.jpg' not found.".to_string()));
211        }
212
213        let resp = bpi.dynamic_upload_pic(test_file, None).await?;
214        let data = resp.into_data()?;
215
216        info!("上传成功!图片 URL: {}", data.image_url);
217
218        tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
219
220        let resp = bpi.message_send(
221            receiver_id,
222            1, // 接收者类型:用户
223            MessageType::Image(Image {
224                url: data.image_url.to_string(),
225                height: data.image_height,
226                width: data.image_width,
227                image_type: None,
228                original: Some(1),
229                size: data.img_size,
230            })
231        ).await?;
232
233        println!("发送私信响应: {:?}", resp);
234        assert_eq!(resp.code, 0);
235
236        Ok(())
237    }
238}