use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
use hmac::{ Hmac, Mac };
use serde::{ Deserialize, Serialize };
use sha2::Sha256;
use std::time::{ SystemTime, UNIX_EPOCH };
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TicketData {
pub ticket: String,
pub created_at: i64,
pub ttl: i32,
pub context: serde_json::Value,
pub nav: NavData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NavData {
pub img: String,
pub sub: String,
}
impl BpiClient {
pub async fn misc_sign_bili_ticket(&self) -> Result<BpiResponse<TicketData>, BpiError> {
let csrf = self.csrf()?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| BpiError::network(format!("获取时间戳失败: {}", e)))?
.as_secs();
let message = format!("ts{}", timestamp);
let hexsign = self.hmac_sha256("XgwSnGZ1p", &message)?;
let params = [
("key_id", "ec02"),
("hexsign", &hexsign),
("context[ts]", ×tamp.to_string()),
("csrf", csrf.as_str()),
];
self
.post("https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket")
.query(¶ms)
.send_bpi("生成bili_ticket").await
}
pub async fn misc_sign_bili_ticket_string(&self) -> Result<String, BpiError> {
let resp = self.misc_sign_bili_ticket().await?;
let data = resp.data.ok_or_else(BpiError::missing_data)?;
Ok(data.ticket)
}
fn hmac_sha256(&self, key: &str, message: &str) -> Result<String, BpiError> {
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(|e|
BpiError::parse(format!("HMAC 密钥错误: {}", e))
)?;
mac.update(message.as_bytes());
let result = mac.finalize();
Ok(hex::encode(result.into_bytes()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_hmac_sha256() {
let bpi = BpiClient::new();
let result = bpi.hmac_sha256("XgwSnGZ1p", "ts1234567890").unwrap();
assert_eq!(result.len(), 64);
assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
tracing::info!("HMAC-SHA256 测试通过: {}", result);
}
#[tokio::test]
async fn test_generate_bili_ticket() {
let bpi = BpiClient::new();
match bpi.misc_sign_bili_ticket().await {
Ok(resp) => {
if resp.code == 0 {
let data = resp.data.unwrap();
tracing::info!("Ticket: {}", data.ticket);
tracing::info!("创建时间: {}", data.created_at);
tracing::info!(
"有效时长: {} 秒 ({:.1} 天)",
data.ttl,
(data.ttl as f64) / 86400.0
);
tracing::info!("WBI img: {}", data.nav.img);
tracing::info!("WBI sub: {}", data.nav.sub);
assert!(data.ticket.contains('.'));
assert!(data.ttl > 250000); } else {
panic!("API 返回错误: code={}, message={}", resp.code, resp.message);
}
}
Err(err) => {
panic!("生成 bili_ticket 失败: {}", err);
}
}
}
#[tokio::test]
async fn test_get_bili_ticket_string() {
let bpi = BpiClient::new();
match bpi.misc_sign_bili_ticket_string().await {
Ok(ticket) => {
tracing::info!("获取到的 bili_ticket: {}", ticket);
assert!(!ticket.is_empty());
assert!(ticket.contains('.'));
let parts: Vec<&str> = ticket.split('.').collect();
assert_eq!(parts.len(), 3);
}
Err(err) => {
panic!("获取 bili_ticket 字符串失败: {}", err);
}
}
}
#[tokio::test]
async fn test_with_csrf() {
let bpi = BpiClient::new();
match bpi.misc_sign_bili_ticket().await {
Ok(resp) => {
tracing::info!("带 CSRF 的 bili_ticket 生成成功: {}", resp.data.unwrap().ticket);
}
Err(err) => {
tracing::info!("带 CSRF 测试失败(预期可能失败): {}", err);
}
}
}
}