use crate::models::{ LevelInfo, Nameplate, Official, OfficialVerify, Pendant, Vip, VipLabel };
use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
use serde::{ Deserialize, Serialize };
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserSpaceInfo {
pub mid: u64,
pub name: String,
pub sex: String,
pub face: String,
pub face_nft: u8,
pub face_nft_type: Option<u8>,
pub sign: String,
pub rank: u32,
pub level: u8,
pub jointime: u64,
pub moral: u64,
pub silence: u8,
pub coins: f64,
pub fans_badge: bool,
pub fans_medal: Option<FansMedal>,
pub official: Official,
pub vip: Vip,
pub pendant: Pendant,
pub nameplate: Nameplate,
pub user_honour_info: UserHonourInfo,
pub is_followed: bool,
pub top_photo: String,
pub theme: serde_json::Value,
pub sys_notice: SysNotice,
pub live_room: LiveRoom,
pub birthday: String,
pub school: School,
pub profession: Option<Profession>,
pub tags: Option<Vec<String>>,
pub series: Series,
pub is_senior_member: u8,
pub mcn_info: Option<serde_json::Value>,
pub gaia_res_type: Option<u8>,
pub gaia_data: Option<serde_json::Value>,
pub is_risk: bool,
pub elec: Elec,
pub contract: Contract,
pub certificate_show: Option<bool>,
pub name_render: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FansMedal {
pub show: bool,
pub wear: bool,
pub medal: Option<Medal>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Medal {
pub level: u8,
pub guard_level: u8,
pub medal_name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserHonourInfo {
pub mid: u64,
pub colour: Option<String>,
pub tags: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SysNotice {
pub id: Option<u32>,
pub content: Option<String>,
pub url: Option<String>,
pub notice_type: Option<u8>,
pub icon: Option<String>,
pub text_color: Option<String>,
pub bg_color: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LiveRoom {
#[serde(rename = "roomStatus")]
pub room_status: u8,
#[serde(rename = "liveStatus")]
pub live_status: u8,
pub url: String,
pub title: String,
pub cover: String,
pub watched_show: WatchedShow,
pub roomid: u64,
#[serde(rename = "roundStatus")]
pub round_status: u8,
pub broadcast_type: u8,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WatchedShow {
pub switch: bool,
pub num: u64,
pub text_small: String,
pub text_large: String,
pub icon: String,
pub icon_location: String,
pub icon_web: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct School {
pub name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Profession {
pub name: String,
pub department: String,
pub title: String,
pub is_show: u8,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Series {
pub user_upgrade_status: u8,
pub show_upgrade_window: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Elec {
pub show_info: ShowInfo,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ShowInfo {
pub show: bool,
pub state: i8,
pub title: String,
pub icon: String,
pub jump_url: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Contract {
pub is_display: bool,
pub is_follow_display: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserCardInfo {
pub card: Card,
pub following: bool,
pub archive_count: u32,
pub article_count: u32,
pub follower: u32,
pub like_num: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Card {
pub mid: String,
pub name: String,
pub sex: String,
pub face: String,
#[serde(rename = "DisplayRank")]
pub display_rank: String,
pub regtime: u64,
pub spacesta: i32,
pub birthday: String,
pub place: String,
pub description: String,
pub article: u32,
pub attentions: Vec<serde_json::Value>,
pub fans: u32,
pub friend: u32,
pub attention: u32,
pub sign: String,
pub level_info: LevelInfo,
pub pendant: Pendant,
pub nameplate: Nameplate,
#[serde(rename = "Official")]
pub official: Official,
pub official_verify: OfficialVerify,
pub vip: Vip,
pub space: Option<Space>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Space {
pub s_img: String,
pub l_img: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserCard {
pub mid: u64,
pub name: String,
pub face: String,
pub sign: String,
pub rank: i32,
pub level: i32,
pub silence: i32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserInfo {
pub mid: u64,
pub name: String,
pub sign: String,
pub rank: i32,
pub level: i32,
pub silence: i32,
pub sex: Option<String>,
pub face: String,
pub vip: Option<UserVip>,
pub official: Option<UserOfficial>,
pub is_fake_account: Option<u32>,
pub expert_info: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserVip {
pub r#type: i32,
pub status: i32,
pub due_date: i64,
pub vip_pay_type: i32,
pub theme_type: i32,
pub label: Option<VipLabel>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserOfficial {
#[serde(default)]
pub role: i32,
#[serde(default)]
pub title: String,
#[serde(default)]
pub desc: String,
#[serde(default)]
pub r#type: i32,
}
impl BpiClient {
pub async fn user_space_info(&self, mid: u64) -> Result<BpiResponse<UserSpaceInfo>, BpiError> {
let params = vec![("mid", mid.to_string())];
let params = self.get_wbi_sign2(params).await?;
self
.get("https://api.bilibili.com/x/space/wbi/acc/info")
.query(¶ms)
.send_bpi("获取用户空间详细信息").await
}
pub async fn user_card_info(
&self,
mid: u64,
photo: Option<bool>
) -> Result<BpiResponse<UserCardInfo>, BpiError> {
let mut params = vec![("mid", mid.to_string())];
if let Some(photo_value) = photo {
params.push(("photo", photo_value.to_string()));
}
self
.get("https://api.bilibili.com/x/web-interface/card")
.query(¶ms)
.send_bpi("获取用户名片信息").await
}
pub async fn user_card_info_with_photo(
&self,
mid: u64
) -> Result<BpiResponse<UserCardInfo>, BpiError> {
self.user_card_info(mid, Some(true)).await
}
pub async fn user_card_info_without_photo(
&self,
mid: u64
) -> Result<BpiResponse<UserCardInfo>, BpiError> {
self.user_card_info(mid, Some(false)).await
}
pub async fn user_cards(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserCard>>, BpiError> {
let mids_str = mids
.iter()
.map(|m| m.to_string())
.collect::<Vec<_>>()
.join(",");
self
.get("https://api.vc.bilibili.com/account/v1/user/cards")
.query(&[("uids", mids_str)])
.send_bpi("批量获取用户卡片").await
}
pub async fn user_infos(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserInfo>>, BpiError> {
let mids_str = mids
.iter()
.map(|m| m.to_string())
.collect::<Vec<_>>()
.join(",");
self
.get("https://api.vc.bilibili.com/x/im/user_infos")
.query(&[("uids", mids_str)])
.send_bpi("批量获取用户详细信息").await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_user_space_info() {
tracing::info!("开始测试获取用户空间详细信息");
let bpi = BpiClient::new();
let mid = 2;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_space_info(mid).await;
match &resp {
Ok(response) => {
let data = response.clone().data.unwrap();
tracing::info!("请求成功,返回码: {}", response.code);
tracing::info!("用户昵称: {}", data.name);
tracing::info!("用户等级: {}", data.level);
tracing::info!("是否为会员: {}", data.vip.vip_type > 0);
tracing::info!(
"粉丝数量: {}",
data.fans_medal.as_ref().map_or(0, |_| 1)
);
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
assert!(resp.is_ok());
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_get_user_space_info_nonexistent() {
tracing::info!("开始测试获取不存在用户的空间详细信息");
let bpi = BpiClient::new();
let mid = 0;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_space_info(mid).await;
match &resp {
Ok(response) => {
tracing::info!("请求返回码: {}", response.code);
tracing::info!("错误信息: {}", response.message);
assert_eq!(response.code, -404);
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_get_user_card_info() {
tracing::info!("开始测试获取用户名片信息");
let bpi = BpiClient::new();
let mid = 2;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_card_info(mid, None).await;
match &resp {
Ok(response) => {
let data = response.clone().data.unwrap();
tracing::info!("请求成功,返回码: {}", response.code);
tracing::info!("用户昵称: {}", data.card.name);
tracing::info!("用户性别: {}", data.card.sex);
tracing::info!("用户等级: {}", data.card.level_info.current_level);
tracing::info!("是否关注: {}", data.following);
tracing::info!("稿件数: {}", data.archive_count);
tracing::info!("粉丝数: {}", data.follower);
tracing::info!("点赞数: {}", data.like_num);
tracing::info!("用户签名: {}", data.card.sign);
let official = &data.card.official;
if official.r#type >= 0 {
tracing::info!("认证类型: {}", official.r#type);
tracing::info!("认证信息: {}", official.title);
} else {
tracing::info!("用户未认证");
}
let vip = &data.card.vip;
if vip.vip_status > 0 {
tracing::info!("大会员状态: 已开通");
tracing::info!("大会员类型: {}", vip.vip_type);
} else {
tracing::info!("大会员状态: 未开通");
}
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
assert!(resp.is_ok());
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_get_user_card_info_with_photo() {
tracing::info!("开始测试获取用户名片信息(包含主页头图)");
let bpi = BpiClient::new();
let mid = 2;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_card_info_with_photo(mid).await;
match &resp {
Ok(response) => {
let data = response.clone().data.unwrap();
tracing::info!("请求成功,返回码: {}", response.code);
tracing::info!("用户昵称: {}", data.card.name);
if let Some(space) = &data.card.space {
tracing::info!("主页头图(小): {}", space.s_img);
tracing::info!("主页头图(正常): {}", space.l_img);
} else {
tracing::info!("用户没有设置主页头图");
}
let pendant = &data.card.pendant;
if pendant.pid > 0 {
tracing::info!("挂件名称: {}", pendant.name);
tracing::info!("挂件图片: {}", pendant.image);
} else {
tracing::info!("用户没有佩戴挂件");
}
let nameplate = &data.card.nameplate;
if nameplate.nid > 0 {
tracing::info!("勋章名称: {}", nameplate.name);
tracing::info!("勋章等级: {}", nameplate.level);
} else {
tracing::info!("用户没有佩戴勋章");
}
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
assert!(resp.is_ok());
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_get_user_card_info_without_photo() {
tracing::info!("开始测试获取用户名片信息(不包含主页头图)");
let bpi = BpiClient::new();
let mid = 123456;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_card_info_without_photo(mid).await;
match &resp {
Ok(response) => {
let data = response.clone().data.unwrap();
tracing::info!("请求成功,返回码: {}", response.code);
tracing::info!("用户昵称: {}", data.card.name);
tracing::info!("粉丝数: {}", data.card.fans);
tracing::info!("关注数: {}", data.card.attention);
if data.card.space.is_none() {
tracing::info!("正确:没有返回主页头图信息");
} else {
tracing::warn!("注意:返回了主页头图信息");
}
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
assert!(resp.is_ok());
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_get_user_card_info_invalid_user() {
tracing::info!("开始测试获取不存在用户的名片信息");
let bpi = BpiClient::new();
let mid = 0;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_card_info(mid, None).await;
match &resp {
Ok(response) => {
tracing::info!("请求返回码: {}", response.code);
tracing::info!("错误信息: {}", response.message);
if response.code != 0 {
tracing::info!("正确:返回了错误码 {}", response.code);
}
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_get_user_card_info_banned_user() {
tracing::info!("开始测试获取被封禁用户的名片信息");
let bpi = BpiClient::new();
let mid = 999999999;
tracing::info!("测试用户ID: {}", mid);
let resp = bpi.user_card_info(mid, None).await;
match &resp {
Ok(response) => {
tracing::info!("请求成功,返回码: {}", response.code);
if response.code == 0 {
let data = response.clone().data.unwrap();
let spacesta = data.card.spacesta;
if spacesta == -2 {
tracing::info!("用户状态: 被封禁");
} else if spacesta == 0 {
tracing::info!("用户状态: 正常");
} else {
tracing::info!("用户状态: 未知 ({})", spacesta);
}
}
}
Err(e) => {
tracing::error!("请求失败: {:?}", e);
}
}
tracing::info!("测试完成");
}
#[tokio::test]
async fn test_user_cards_and_infos() {
let bpi = BpiClient::new();
let cards = bpi.user_cards(&[2, 3]).await.unwrap();
tracing::info!("用户卡片: {:?}", cards.data);
let infos = bpi.user_infos(&[2, 3]).await.unwrap();
tracing::info!("用户详细信息: {:?}", infos.data);
}
}