bpi_rs/login/login_action/
sms.rs1use crate::ApiEnvelope;
2use crate::BilibiliRequest;
3use crate::BpiError;
4use crate::BpiResult;
5use crate::login::LoginClient;
6use reqwest::header::SET_COOKIE;
7use serde::{Deserialize, Serialize};
8use tracing::{error, info};
9
10#[derive(Debug, Deserialize, Serialize)]
11pub struct SMSSendData {
12 captcha_key: String, }
14
15#[derive(Debug, Deserialize, Serialize)]
16struct SMSLoginData {
17 is_new: bool, status: i32, url: String, }
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct LoginSmsCodeParams {
25 cid: u32,
26 tel: String,
27 source: String,
28 token: String,
29 challenge: String,
30 validate: String,
31 seccode: String,
32}
33
34impl LoginSmsCodeParams {
35 pub fn new(
37 cid: u32,
38 tel: impl Into<String>,
39 token: impl Into<String>,
40 challenge: impl Into<String>,
41 validate: impl Into<String>,
42 seccode: impl Into<String>,
43 ) -> BpiResult<Self> {
44 let params = Self {
45 cid,
46 tel: tel.into(),
47 source: "main_web".to_string(),
48 token: token.into(),
49 challenge: challenge.into(),
50 validate: validate.into(),
51 seccode: seccode.into(),
52 };
53 params.validate()?;
54 Ok(params)
55 }
56
57 pub fn source(mut self, source: impl Into<String>) -> BpiResult<Self> {
59 self.source = source.into();
60 self.validate()?;
61 Ok(self)
62 }
63
64 fn validate(&self) -> BpiResult<()> {
65 if self.cid == 0 {
66 return Err(BpiError::invalid_parameter("cid", "cid must be non-zero"));
67 }
68 validate_required("tel", &self.tel)?;
69 validate_required("source", &self.source)?;
70 validate_required("token", &self.token)?;
71 validate_required("challenge", &self.challenge)?;
72 validate_required("validate", &self.validate)?;
73 validate_required("seccode", &self.seccode)?;
74 Ok(())
75 }
76
77 fn form_pairs(&self) -> Vec<(&'static str, String)> {
78 vec![
79 ("cid", self.cid.to_string()),
80 ("tel", self.tel.clone()),
81 ("source", self.source.clone()),
82 ("token", self.token.clone()),
83 ("challenge", self.challenge.clone()),
84 ("validate", self.validate.clone()),
85 ("seccode", self.seccode.clone()),
86 ]
87 }
88}
89
90fn validate_required(field: &'static str, value: &str) -> BpiResult<()> {
91 if value.trim().is_empty() {
92 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
93 }
94
95 Ok(())
96}
97
98impl<'a> LoginClient<'a> {
99 pub async fn send_sms_code(&self, params: LoginSmsCodeParams) -> BpiResult<SMSSendData> {
101 self.client
102 .post("https://passport.bilibili.com/x/passport-login/web/sms/send")
103 .form(¶ms.form_pairs())
104 .send_bpi_payload("login.sms.send")
105 .await
106 }
107
108 pub async fn login_with_sms(
115 &self,
116 cid: u32,
117 tel: u32,
118 captcha_key: &str,
119 code: &str,
120 ) -> Result<(), String> {
121 let form = vec![
122 ("cid", cid.to_string()),
123 ("tel", tel.to_string()),
124 ("code", code.to_string()),
125 ("source", "main_web".to_string()),
126 ("captcha_key", captcha_key.to_string()),
127 ("go_url", "https://www.bilibili.com".to_string()),
128 ("keep", true.to_string()),
129 ];
130
131 let response = self
132 .client
133 .post("https://passport.bilibili.com/x/passport-login/web/login/sms")
134 .form(&form)
135 .send()
136 .await
137 .map_err(|e| e.to_string())?;
138
139 if let Some(cookies) = response
140 .headers()
141 .get_all(SET_COOKIE)
142 .iter()
143 .map(|v| v.to_str().unwrap_or(""))
144 .collect::<Vec<_>>()
145 .join("; ")
146 .into()
147 {
148 info!("登录返回的 Cookie: {}", cookies);
149 }
150
151 let resp = response
152 .json::<ApiEnvelope<SMSLoginData>>()
153 .await
154 .map_err(|e| {
155 error!("解析短信登录响应失败: {:?}", e);
156 e.to_string()
157 })?;
158
159 if resp.code != 0 {
160 error!("短信登录失败: code={}, message={}", resp.code, resp.message);
161 return Err(resp.message);
162 }
163
164 match resp.code {
165 0 => {
166 info!("短信登录成功");
167 Ok(())
168 }
169 code => {
170 error!("验证码发送失败: code={}, message={}", code, resp.message);
171 let msg = match code {
172 -400 => "请求错误".to_string(),
173 1006 => "请输入正确的短信验证码".to_string(),
174 1007 => "短信验证码已过期".to_string(),
175
176 _ => resp.message,
177 };
178 Err(msg)
179 }
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn login_sms_code_params_rejects_blank_token() {
190 let err = LoginSmsCodeParams::new(
191 86,
192 "13800138000",
193 " ",
194 "challenge",
195 "validate",
196 "validate|jordan",
197 )
198 .unwrap_err();
199
200 assert!(matches!(
201 err,
202 BpiError::InvalidParameter { field: "token", .. }
203 ));
204 }
205}