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    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/message
73    ///
74    /// 参数
75    ///
76    /// | 名称 | 类型 | 说明 |
77    /// | ---- | ---- | ---- |
78    /// | `unread_type` | Option<u32> | 未读类型(默认 All) |
79    /// | `show_unfollow_list` | Option<u32> | 是否返回未关注推送消息数 |
80    /// | `show_dustbin` | Option<u32> | 是否返回被拦截私信数 |
81    ///
82    /// 备注:若 `unread_type` 为 Blocked,`show_dustbin` 必须为 true。
83    pub async fn message_single_unread(
84        &self,
85        unread_type: Option<u32>,
86        show_unfollow_list: Option<u32>,
87        show_dustbin: Option<u32>,
88    ) -> Result<BpiResponse<SingleUnreadData>, BpiError> {
89        let params = [
90            ("build", "0"),
91            ("mobi_app", "web"),
92            (
93                "unread_type",
94                &unread_type.map_or("0".to_string(), |v| v.to_string()),
95            ),
96            (
97                "show_unfollow_list",
98                if show_unfollow_list == Some(1) {
99                    "1"
100                } else {
101                    "0"
102                },
103            ),
104            (
105                "show_dustbin",
106                if show_dustbin.is_some() { "1" } else { "0" },
107            ),
108        ];
109
110        self.get("https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread")
111            .query(&params)
112            .send_bpi("获取未读私信数")
113            .await
114    }
115
116    /// 发送私信。
117    ///
118    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/message
119    ///
120    /// 参数
121    ///
122    /// | 名称 | 类型 | 说明 |
123    /// | ---- | ---- | ---- |
124    /// | `receiver_id` | u64 | 接收者 ID |
125    /// | `receiver_type` | u32 | 接收者类型:1 用户,2 粉丝团 |
126    /// | `message_type` | MessageType | 消息类型(文本/图片) |
127    pub async fn message_send(
128        &self,
129        receiver_id: u64,
130        receiver_type: u32,
131        message_type: MessageType,
132    ) -> Result<BpiResponse<SendMsgData>, BpiError> {
133        // 1. 获取必需的参数
134        let csrf = self.csrf()?;
135        let sender_uid = &self
136            .get_account()
137            .ok_or(BpiError::auth("未登录"))?
138            .dede_user_id;
139        let dev_id = Uuid::new_v4().to_string();
140        let timestamp = Utc::now().timestamp();
141
142        let msg_type = match message_type {
143            MessageType::Text(_) => 1,
144            MessageType::Image(_) => 2,
145        };
146
147        // 2. 准备请求体参数
148        let mut form = vec![
149            ("msg[sender_uid]", sender_uid.to_string()),
150            ("msg[receiver_id]", receiver_id.to_string()),
151            ("msg[receiver_type]", receiver_type.to_string()),
152            ("msg[msg_type]", msg_type.to_string()),
153            ("msg[msg_status]", "0".to_string()),
154            ("msg[dev_id]", dev_id.clone()),
155            ("msg[timestamp]", timestamp.to_string()),
156            ("msg[new_face_version]", "1".to_string()),
157            ("csrf", csrf.clone()),
158            ("csrf_token", csrf.clone()),
159            ("build", "0".to_string()),
160            ("mobi_app", "web".to_string()),
161        ];
162
163        // 3. 构造 msg[content] 参数
164        let content = match message_type {
165            MessageType::Text(text) => json!({ "content": text }).to_string(),
166            MessageType::Image(image) => serde_json::to_string(&image)?,
167        };
168
169        form.push(("msg[content]", content));
170
171        let params = vec![
172            ("w_sender_uid", sender_uid.to_string()),
173            ("w_receiver_id", receiver_id.to_string()),
174            ("w_dev_id", dev_id.clone()),
175        ];
176
177        let signed_params = self.get_wbi_sign2(params).await?;
178
179        // 发送请求
180        self.post("https://api.vc.bilibili.com/web_im/v1/web_im/send_msg")
181            .query(&signed_params)
182            .form(&form)
183            .send_bpi("发送私信")
184            .await
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use std::path::Path;
192    use tracing::info;
193
194    #[tokio::test]
195
196    async fn test_get_single_unread() -> Result<(), BpiError> {
197        let bpi = BpiClient::new();
198
199        // 默认查询所有未读私信数
200        let all_unread_resp = bpi.message_single_unread(None, None, None).await?;
201        let all_unread_data = all_unread_resp.into_data()?;
202        println!("所有未读私信数: {:?}", all_unread_data);
203
204        assert_eq!(all_unread_data.dustbin_unread, 0); // show_dustbin为false时,该值为0
205
206        Ok(())
207    }
208
209    #[tokio::test]
210
211    async fn test_send_text_message() -> Result<(), BpiError> {
212        let bpi = BpiClient::new();
213        let receiver_id = 107997089; // 替换为你要发送消息的目标用户mid
214        // let message_content = "这是一个测试消息。";
215        //
216        // let resp = bpi
217        //     .send_message(
218        //         receiver_id,
219        //         1, // 接收者类型:用户
220        //         MessageType::Text(message_content),
221        //     )
222        //     .await?;
223
224        let test_file = Path::new("./assets/test.jpg");
225        if !test_file.exists() {
226            return Err(BpiError::parse(
227                "Test file 'test.jpg' not found.".to_string(),
228            ));
229        }
230
231        let resp = bpi.dynamic_upload_pic(test_file, None).await?;
232        let data = resp.into_data()?;
233
234        info!("上传成功!图片 URL: {}", data.image_url);
235
236        tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
237
238        let resp = bpi
239            .message_send(
240                receiver_id,
241                1, // 接收者类型:用户
242                MessageType::Image(Image {
243                    url: data.image_url.to_string(),
244                    height: data.image_height,
245                    width: data.image_width,
246                    image_type: None,
247                    original: Some(1),
248                    size: data.img_size,
249                }),
250            )
251            .await?;
252
253        println!("发送私信响应: {:?}", resp);
254        assert_eq!(resp.code, 0);
255
256        Ok(())
257    }
258}