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(
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(¶ms)
101 .send_bpi("获取未读私信数").await
102 }
103
104 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 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 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 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 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 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); 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; 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, 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}