1use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4
5use chrono::Utc;
6use uuid::Uuid;
7
8#[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#[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#[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#[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>>, }
48
49#[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>, pub size: f64,
59}
60
61pub enum MessageType {
63 Text(String),
65 Image(Image),
67}
68
69impl BpiClient {
70 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(¶ms)
112 .send_bpi("获取未读私信数")
113 .await
114 }
115
116 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 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 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 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 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 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); 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; 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, 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}