1use crate::models::{LevelInfo, Nameplate, Official, OfficialVerify, Pendant, Vip, VipLabel};
5use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct UserSpaceInfo {
11 pub mid: u64,
13 pub name: String,
15 pub sex: String,
17 pub face: String,
19 pub face_nft: u8,
21 pub face_nft_type: Option<u8>,
23 pub sign: String,
25 pub rank: u32,
27 pub level: u8,
29 pub jointime: u64,
31 pub moral: u64,
33 pub silence: u8,
35 pub coins: f64,
37 pub fans_badge: bool,
39 pub fans_medal: Option<FansMedal>,
41 pub official: Official,
43 pub vip: Vip,
45 pub pendant: Pendant,
47 pub nameplate: Nameplate,
49 pub user_honour_info: UserHonourInfo,
51 pub is_followed: bool,
53 pub top_photo: String,
55 pub theme: serde_json::Value,
57 pub sys_notice: SysNotice,
59 pub live_room: LiveRoom,
61 pub birthday: String,
63 pub school: School,
65 pub profession: Option<Profession>,
67 pub tags: Option<Vec<String>>,
69 pub series: Series,
71 pub is_senior_member: u8,
73 pub mcn_info: Option<serde_json::Value>,
75 pub gaia_res_type: Option<u8>,
77 pub gaia_data: Option<serde_json::Value>,
79 pub is_risk: bool,
81 pub elec: Elec,
83 pub contract: Contract,
85 pub certificate_show: Option<bool>,
87 pub name_render: Option<serde_json::Value>,
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize)]
93pub struct FansMedal {
94 pub show: bool,
96 pub wear: bool,
98 pub medal: Option<Medal>,
100}
101
102#[derive(Debug, Clone, Deserialize, Serialize)]
104pub struct Medal {
105 pub level: u8,
107 pub guard_level: u8,
109 pub medal_name: String,
111}
112
113#[derive(Debug, Clone, Deserialize, Serialize)]
115pub struct UserHonourInfo {
116 pub mid: u64,
118 pub colour: Option<String>,
120 pub tags: Option<Vec<serde_json::Value>>,
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize)]
126pub struct SysNotice {
127 pub id: Option<u32>,
129 pub content: Option<String>,
131 pub url: Option<String>,
133 pub notice_type: Option<u8>,
135 pub icon: Option<String>,
137 pub text_color: Option<String>,
139 pub bg_color: Option<String>,
141}
142
143#[derive(Debug, Clone, Deserialize, Serialize)]
145pub struct LiveRoom {
146 #[serde(rename = "roomStatus")]
148 pub room_status: u8,
149 #[serde(rename = "liveStatus")]
151 pub live_status: u8,
152 pub url: String,
154 pub title: String,
156 pub cover: String,
158 pub watched_show: WatchedShow,
160 pub roomid: u64,
162 #[serde(rename = "roundStatus")]
164 pub round_status: u8,
165 pub broadcast_type: u8,
167}
168
169#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct WatchedShow {
172 pub switch: bool,
174 pub num: u64,
176 pub text_small: String,
178 pub text_large: String,
180 pub icon: String,
182 pub icon_location: String,
184 pub icon_web: String,
186}
187
188#[derive(Debug, Clone, Deserialize, Serialize)]
190pub struct School {
191 pub name: String,
193}
194
195#[derive(Debug, Clone, Deserialize, Serialize)]
197pub struct Profession {
198 pub name: String,
200 pub department: String,
202 pub title: String,
204 pub is_show: u8,
206}
207
208#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct Series {
211 pub user_upgrade_status: u8,
213 pub show_upgrade_window: bool,
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize)]
219pub struct Elec {
220 pub show_info: ShowInfo,
222}
223
224#[derive(Debug, Clone, Deserialize, Serialize)]
226pub struct ShowInfo {
227 pub show: bool,
229 pub state: i8,
231 pub title: String,
233 pub icon: String,
235 pub jump_url: String,
237}
238
239#[derive(Debug, Clone, Deserialize, Serialize)]
241pub struct Contract {
242 pub is_display: bool,
244 pub is_follow_display: bool,
246}
247
248#[derive(Debug, Clone, Deserialize, Serialize)]
250pub struct UserCardInfo {
251 pub card: Card,
253 pub following: bool,
255 pub archive_count: u32,
257 pub article_count: u32,
259 pub follower: u32,
261 pub like_num: u32,
263}
264
265#[derive(Debug, Clone, Deserialize, Serialize)]
267pub struct Card {
268 pub mid: String,
270 pub name: String,
272 pub sex: String,
274 pub face: String,
276 #[serde(rename = "DisplayRank")]
278 pub display_rank: String,
279 pub regtime: u64,
281 pub spacesta: i32,
283 pub birthday: String,
285 pub place: String,
287 pub description: String,
289 pub article: u32,
291 pub attentions: Vec<serde_json::Value>,
293 pub fans: u32,
295 pub friend: u32,
297 pub attention: u32,
299 pub sign: String,
301 pub level_info: LevelInfo,
303 pub pendant: Pendant,
305 pub nameplate: Nameplate,
307 #[serde(rename = "Official")]
309 pub official: Official,
310 pub official_verify: OfficialVerify,
312 pub vip: Vip,
314 pub space: Option<Space>,
316}
317
318#[derive(Debug, Clone, Deserialize, Serialize)]
320pub struct Space {
321 pub s_img: String,
323 pub l_img: String,
325}
326
327#[derive(Debug, Clone, Deserialize, Serialize)]
329pub struct UserCard {
330 pub mid: u64,
331 pub name: String,
332 pub face: String,
333 pub sign: String,
334 pub rank: i32,
335 pub level: i32,
336 pub silence: i32,
337}
338
339#[derive(Debug, Clone, Deserialize, Serialize)]
341pub struct UserInfo {
342 pub mid: u64,
343 pub name: String,
344 pub sign: String,
345 pub rank: i32,
346 pub level: i32,
347 pub silence: i32,
348
349 pub sex: Option<String>,
350 pub face: String,
351 pub vip: Option<UserVip>,
352 pub official: Option<UserOfficial>,
353 pub is_fake_account: Option<u32>,
354 pub expert_info: Option<serde_json::Value>,
355}
356
357#[derive(Debug, Clone, Deserialize, Serialize)]
359pub struct UserVip {
360 pub r#type: i32,
361 pub status: i32,
362 pub due_date: i64,
363 pub vip_pay_type: i32,
364 pub theme_type: i32,
365 pub label: Option<VipLabel>,
366}
367
368#[derive(Debug, Clone, Deserialize, Serialize)]
370pub struct UserOfficial {
371 #[serde(default)]
372 pub role: i32,
373 #[serde(default)]
374 pub title: String,
375 #[serde(default)]
376 pub desc: String,
377 #[serde(default)]
378 pub r#type: i32,
379}
380
381impl BpiClient {
382 pub async fn user_space_info(&self, mid: u64) -> Result<BpiResponse<UserSpaceInfo>, BpiError> {
389 let params = vec![("mid", mid.to_string())];
391
392 let params = self.get_wbi_sign2(params).await?;
393
394 self.get("https://api.bilibili.com/x/space/wbi/acc/info")
395 .query(¶ms)
396 .send_bpi("获取用户空间详细信息")
397 .await
398 }
399
400 pub async fn user_card_info(
411 &self,
412 mid: u64,
413 photo: Option<bool>,
414 ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
415 let mut params = vec![("mid", mid.to_string())];
416
417 if let Some(photo_value) = photo {
419 params.push(("photo", photo_value.to_string()));
420 }
421
422 self.get("https://api.bilibili.com/x/web-interface/card")
423 .query(¶ms)
424 .send_bpi("获取用户名片信息")
425 .await
426 }
427
428 pub async fn user_card_info_with_photo(
434 &self,
435 mid: u64,
436 ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
437 self.user_card_info(mid, Some(true)).await
438 }
439
440 pub async fn user_card_info_without_photo(
446 &self,
447 mid: u64,
448 ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
449 self.user_card_info(mid, Some(false)).await
450 }
451
452 pub async fn user_cards(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserCard>>, BpiError> {
455 let mids_str = mids
456 .iter()
457 .map(|m| m.to_string())
458 .collect::<Vec<_>>()
459 .join(",");
460
461 self.get("https://api.vc.bilibili.com/account/v1/user/cards")
462 .query(&[("uids", mids_str)])
463 .send_bpi("批量获取用户卡片")
464 .await
465 }
466
467 pub async fn user_infos(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserInfo>>, BpiError> {
470 let mids_str = mids
471 .iter()
472 .map(|m| m.to_string())
473 .collect::<Vec<_>>()
474 .join(",");
475
476 self.get("https://api.vc.bilibili.com/x/im/user_infos")
477 .query(&[("uids", mids_str)])
478 .send_bpi("批量获取用户详细信息")
479 .await
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[tokio::test]
488 async fn test_get_user_space_info() {
489 tracing::info!("开始测试获取用户空间详细信息");
490
491 let bpi = BpiClient::new();
492 let mid = 2; tracing::info!("测试用户ID: {}", mid);
495
496 let resp = bpi.user_space_info(mid).await;
497
498 match &resp {
499 Ok(response) => {
500 let data = response.clone().data.unwrap();
501 tracing::info!("请求成功,返回码: {}", response.code);
502 tracing::info!("用户昵称: {}", data.name);
503 tracing::info!("用户等级: {}", data.level);
504 tracing::info!("是否为会员: {}", data.vip.vip_type > 0);
505 tracing::info!("粉丝数量: {}", data.fans_medal.as_ref().map_or(0, |_| 1));
506 }
507 Err(e) => {
508 tracing::error!("请求失败: {:?}", e);
509 }
510 }
511
512 assert!(resp.is_ok());
513 tracing::info!("测试完成");
514 }
515
516 #[tokio::test]
517 async fn test_get_user_space_info_nonexistent() {
518 tracing::info!("开始测试获取不存在用户的空间详细信息");
519
520 let bpi = BpiClient::new();
521 let mid = 0; tracing::info!("测试用户ID: {}", mid);
524
525 let resp = bpi.user_space_info(mid).await;
526
527 match &resp {
528 Ok(response) => {
529 tracing::info!("请求返回码: {}", response.code);
530 tracing::info!("错误信息: {}", response.message);
531 assert_eq!(response.code, -404);
533 }
534 Err(e) => {
535 tracing::error!("请求失败: {:?}", e);
536 }
537 }
538
539 tracing::info!("测试完成");
540 }
541
542 #[tokio::test]
543 async fn test_get_user_card_info() {
544 tracing::info!("开始测试获取用户名片信息");
545
546 let bpi = BpiClient::new();
547 let mid = 2; tracing::info!("测试用户ID: {}", mid);
550
551 let resp = bpi.user_card_info(mid, None).await;
552
553 match &resp {
554 Ok(response) => {
555 let data = response.clone().data.unwrap();
556
557 tracing::info!("请求成功,返回码: {}", response.code);
558 tracing::info!("用户昵称: {}", data.card.name);
559 tracing::info!("用户性别: {}", data.card.sex);
560 tracing::info!("用户等级: {}", data.card.level_info.current_level);
561 tracing::info!("是否关注: {}", data.following);
562 tracing::info!("稿件数: {}", data.archive_count);
563 tracing::info!("粉丝数: {}", data.follower);
564 tracing::info!("点赞数: {}", data.like_num);
565 tracing::info!("用户签名: {}", data.card.sign);
566
567 let official = &data.card.official;
569 if official.r#type >= 0 {
570 tracing::info!("认证类型: {}", official.r#type);
571 tracing::info!("认证信息: {}", official.title);
572 } else {
573 tracing::info!("用户未认证");
574 }
575
576 let vip = &data.card.vip;
578 if vip.vip_status > 0 {
579 tracing::info!("大会员状态: 已开通");
580 tracing::info!("大会员类型: {}", vip.vip_type);
581 } else {
582 tracing::info!("大会员状态: 未开通");
583 }
584 }
585 Err(e) => {
586 tracing::error!("请求失败: {:?}", e);
587 }
588 }
589
590 assert!(resp.is_ok());
591 tracing::info!("测试完成");
592 }
593
594 #[tokio::test]
595 async fn test_get_user_card_info_with_photo() {
596 tracing::info!("开始测试获取用户名片信息(包含主页头图)");
597
598 let bpi = BpiClient::new();
599 let mid = 2; tracing::info!("测试用户ID: {}", mid);
602
603 let resp = bpi.user_card_info_with_photo(mid).await;
604
605 match &resp {
606 Ok(response) => {
607 let data = response.clone().data.unwrap();
608
609 tracing::info!("请求成功,返回码: {}", response.code);
610 tracing::info!("用户昵称: {}", data.card.name);
611
612 if let Some(space) = &data.card.space {
614 tracing::info!("主页头图(小): {}", space.s_img);
615 tracing::info!("主页头图(正常): {}", space.l_img);
616 } else {
617 tracing::info!("用户没有设置主页头图");
618 }
619
620 let pendant = &data.card.pendant;
622 if pendant.pid > 0 {
623 tracing::info!("挂件名称: {}", pendant.name);
624 tracing::info!("挂件图片: {}", pendant.image);
625 } else {
626 tracing::info!("用户没有佩戴挂件");
627 }
628
629 let nameplate = &data.card.nameplate;
631 if nameplate.nid > 0 {
632 tracing::info!("勋章名称: {}", nameplate.name);
633 tracing::info!("勋章等级: {}", nameplate.level);
634 } else {
635 tracing::info!("用户没有佩戴勋章");
636 }
637 }
638 Err(e) => {
639 tracing::error!("请求失败: {:?}", e);
640 }
641 }
642
643 assert!(resp.is_ok());
644 tracing::info!("测试完成");
645 }
646
647 #[tokio::test]
648 async fn test_get_user_card_info_without_photo() {
649 tracing::info!("开始测试获取用户名片信息(不包含主页头图)");
650
651 let bpi = BpiClient::new();
652 let mid = 123456; tracing::info!("测试用户ID: {}", mid);
655
656 let resp = bpi.user_card_info_without_photo(mid).await;
657
658 match &resp {
659 Ok(response) => {
660 let data = response.clone().data.unwrap();
661
662 tracing::info!("请求成功,返回码: {}", response.code);
663 tracing::info!("用户昵称: {}", data.card.name);
664 tracing::info!("粉丝数: {}", data.card.fans);
665 tracing::info!("关注数: {}", data.card.attention);
666
667 if data.card.space.is_none() {
669 tracing::info!("正确:没有返回主页头图信息");
670 } else {
671 tracing::warn!("注意:返回了主页头图信息");
672 }
673 }
674 Err(e) => {
675 tracing::error!("请求失败: {:?}", e);
676 }
677 }
678
679 assert!(resp.is_ok());
680 tracing::info!("测试完成");
681 }
682
683 #[tokio::test]
684 async fn test_get_user_card_info_invalid_user() {
685 tracing::info!("开始测试获取不存在用户的名片信息");
686
687 let bpi = BpiClient::new();
688 let mid = 0; tracing::info!("测试用户ID: {}", mid);
691
692 let resp = bpi.user_card_info(mid, None).await;
693
694 match &resp {
695 Ok(response) => {
696 tracing::info!("请求返回码: {}", response.code);
697 tracing::info!("错误信息: {}", response.message);
698 if response.code != 0 {
700 tracing::info!("正确:返回了错误码 {}", response.code);
701 }
702 }
703 Err(e) => {
704 tracing::error!("请求失败: {:?}", e);
705 }
706 }
707
708 tracing::info!("测试完成");
709 }
710
711 #[tokio::test]
712 async fn test_get_user_card_info_banned_user() {
713 tracing::info!("开始测试获取被封禁用户的名片信息");
714
715 let bpi = BpiClient::new();
716 let mid = 999999999; tracing::info!("测试用户ID: {}", mid);
719
720 let resp = bpi.user_card_info(mid, None).await;
721
722 match &resp {
723 Ok(response) => {
724 tracing::info!("请求成功,返回码: {}", response.code);
725
726 if response.code == 0 {
727 let data = response.clone().data.unwrap();
728
729 let spacesta = data.card.spacesta;
730 if spacesta == -2 {
731 tracing::info!("用户状态: 被封禁");
732 } else if spacesta == 0 {
733 tracing::info!("用户状态: 正常");
734 } else {
735 tracing::info!("用户状态: 未知 ({})", spacesta);
736 }
737 }
738 }
739 Err(e) => {
740 tracing::error!("请求失败: {:?}", e);
741 }
742 }
743
744 tracing::info!("测试完成");
745 }
746
747 #[tokio::test]
748 async fn test_user_cards_and_infos() {
749 let bpi = BpiClient::new();
750
751 let cards = bpi.user_cards(&[2, 3]).await.unwrap();
753 tracing::info!("用户卡片: {:?}", cards.data);
754
755 let infos = bpi.user_infos(&[2, 3]).await.unwrap();
757 tracing::info!("用户详细信息: {:?}", infos.data);
758 }
759}