1use crate::models::{LevelInfo, Nameplate, Official, OfficialVerify, Pendant, Vip, VipLabel};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct UserSpaceInfo {
10 pub mid: u64,
12 pub name: String,
14 pub sex: String,
16 pub face: String,
18 pub face_nft: u8,
20 pub face_nft_type: Option<u8>,
22 pub sign: String,
24 pub rank: u32,
26 pub level: u8,
28 pub jointime: u64,
30 pub moral: u64,
32 pub silence: u8,
34 pub coins: f64,
36 pub fans_badge: bool,
38 pub fans_medal: Option<FansMedal>,
40 pub official: Official,
42 pub vip: Vip,
44 pub pendant: Pendant,
46 pub nameplate: Nameplate,
48 pub user_honour_info: UserHonourInfo,
50 pub is_followed: bool,
52 pub top_photo: String,
54 pub theme: serde_json::Value,
56 pub sys_notice: SysNotice,
58 #[serde(default)]
60 pub live_room: Option<LiveRoom>,
61 pub birthday: String,
63 #[serde(default)]
65 pub school: Option<School>,
66 pub profession: Option<Profession>,
68 pub tags: Option<Vec<String>>,
70 pub series: Series,
72 pub is_senior_member: u8,
74 pub mcn_info: Option<serde_json::Value>,
76 pub gaia_res_type: Option<u8>,
78 pub gaia_data: Option<serde_json::Value>,
80 pub is_risk: bool,
82 pub elec: Elec,
84 pub contract: Contract,
86 pub certificate_show: Option<bool>,
88 pub name_render: Option<serde_json::Value>,
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize)]
94pub struct FansMedal {
95 pub show: bool,
97 pub wear: bool,
99 pub medal: Option<Medal>,
101}
102
103#[derive(Debug, Clone, Deserialize, Serialize)]
105pub struct Medal {
106 pub level: u8,
108 pub guard_level: u8,
110 pub medal_name: String,
112}
113
114#[derive(Debug, Clone, Deserialize, Serialize)]
116pub struct UserHonourInfo {
117 pub mid: u64,
119 pub colour: Option<String>,
121 pub tags: Option<Vec<serde_json::Value>>,
123}
124
125#[derive(Debug, Clone, Deserialize, Serialize)]
127pub struct SysNotice {
128 pub id: Option<u32>,
130 pub content: Option<String>,
132 pub url: Option<String>,
134 pub notice_type: Option<u8>,
136 pub icon: Option<String>,
138 pub text_color: Option<String>,
140 pub bg_color: Option<String>,
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
146pub struct LiveRoom {
147 #[serde(rename = "roomStatus")]
149 pub room_status: u8,
150 #[serde(rename = "liveStatus")]
152 pub live_status: u8,
153 pub url: String,
155 pub title: String,
157 pub cover: String,
159 pub watched_show: WatchedShow,
161 pub roomid: u64,
163 #[serde(rename = "roundStatus")]
165 pub round_status: u8,
166 pub broadcast_type: u8,
168}
169
170#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct WatchedShow {
173 pub switch: bool,
175 pub num: u64,
177 pub text_small: String,
179 pub text_large: String,
181 pub icon: String,
183 pub icon_location: String,
185 pub icon_web: String,
187}
188
189#[derive(Debug, Clone, Deserialize, Serialize)]
191pub struct School {
192 pub name: String,
194}
195
196#[derive(Debug, Clone, Deserialize, Serialize)]
198pub struct Profession {
199 pub name: String,
201 pub department: String,
203 pub title: String,
205 pub is_show: u8,
207}
208
209#[derive(Debug, Clone, Deserialize, Serialize)]
211pub struct Series {
212 pub user_upgrade_status: u8,
214 pub show_upgrade_window: bool,
216}
217
218#[derive(Debug, Clone, Deserialize, Serialize)]
220pub struct Elec {
221 pub show_info: ShowInfo,
223}
224
225#[derive(Debug, Clone, Deserialize, Serialize)]
227pub struct ShowInfo {
228 pub show: bool,
230 pub state: i8,
232 pub title: String,
234 pub icon: String,
236 pub jump_url: String,
238}
239
240#[derive(Debug, Clone, Deserialize, Serialize)]
242pub struct Contract {
243 pub is_display: bool,
245 pub is_follow_display: bool,
247}
248
249#[derive(Debug, Clone, Deserialize, Serialize)]
251pub struct UserCardInfo {
252 pub card: Card,
254 pub following: bool,
256 pub archive_count: u32,
258 pub article_count: u32,
260 pub follower: u32,
262 pub like_num: u32,
264}
265
266#[derive(Debug, Clone, Deserialize, Serialize)]
268pub struct Card {
269 pub mid: String,
271 pub name: String,
273 pub sex: String,
275 pub face: String,
277 #[serde(rename = "DisplayRank")]
279 pub display_rank: String,
280 pub regtime: u64,
282 pub spacesta: i32,
284 pub birthday: String,
286 pub place: String,
288 pub description: String,
290 pub article: u32,
292 pub attentions: Vec<serde_json::Value>,
294 pub fans: u32,
296 pub friend: u32,
298 pub attention: u32,
300 pub sign: String,
302 pub level_info: LevelInfo,
304 pub pendant: Pendant,
306 pub nameplate: Nameplate,
308 #[serde(rename = "Official")]
310 pub official: Official,
311 pub official_verify: OfficialVerify,
313 pub vip: Vip,
315 pub space: Option<Space>,
317}
318
319#[derive(Debug, Clone, Deserialize, Serialize)]
321pub struct Space {
322 pub s_img: String,
324 pub l_img: String,
326}
327
328#[derive(Debug, Clone, Deserialize, Serialize)]
330pub struct UserCard {
331 pub mid: u64,
332 pub name: String,
333 pub face: String,
334 pub sign: String,
335 pub rank: i32,
336 pub level: i32,
337 pub silence: i32,
338}
339
340#[derive(Debug, Clone, Deserialize, Serialize)]
342pub struct UserInfo {
343 pub mid: u64,
344 pub name: String,
345 pub sign: String,
346 pub rank: i32,
347 pub level: i32,
348 pub silence: i32,
349
350 pub sex: Option<String>,
351 pub face: String,
352 pub vip: Option<UserVip>,
353 pub official: Option<UserOfficial>,
354 pub is_fake_account: Option<u32>,
355 pub expert_info: Option<serde_json::Value>,
356}
357
358#[derive(Debug, Clone, Deserialize, Serialize)]
360pub struct UserVip {
361 pub r#type: i32,
362 pub status: i32,
363 pub due_date: i64,
364 pub vip_pay_type: i32,
365 pub theme_type: i32,
366 pub label: Option<VipLabel>,
367}
368
369#[derive(Debug, Clone, Deserialize, Serialize)]
371pub struct UserOfficial {
372 #[serde(default)]
373 pub role: i32,
374 #[serde(default)]
375 pub title: String,
376 #[serde(default)]
377 pub desc: String,
378 #[serde(default)]
379 pub r#type: i32,
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::BpiClient;
386 use crate::ids::Mid;
387 use crate::user::params::{
388 UserCardParams, UserCardPhoto, UserCardsParams, UserInfosParams, UserSpaceParams,
389 };
390
391 fn live_user_tests_enabled() -> bool {
392 std::env::var("BPI_LIVE_TEST").ok().as_deref() == Some("1")
393 }
394
395 #[test]
396 fn user_space_info_accepts_null_live_room_and_school() {
397 #[derive(serde::Deserialize)]
398 struct NullableFields {
399 #[serde(default)]
400 live_room: Option<LiveRoom>,
401 #[serde(default)]
402 school: Option<School>,
403 }
404
405 let parsed: NullableFields = serde_json::from_str(
406 r#"{
407 "live_room": null,
408 "school": null
409 }"#,
410 )
411 .expect("nullable user-space fields should deserialize");
412
413 assert!(parsed.live_room.is_none());
414 assert!(parsed.school.is_none());
415 }
416
417 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
418 #[tokio::test]
419 async fn test_get_user_space_info() {
420 if !live_user_tests_enabled() {
421 return;
422 }
423
424 tracing::info!("开始测试获取用户空间详细信息");
425
426 let bpi = BpiClient::new().expect("client should build");
427 let mid = 2; tracing::info!("测试用户ID: {}", mid);
430
431 let resp = bpi
432 .user()
433 .space_info(UserSpaceParams::new(Mid::new(mid).expect("valid mid")))
434 .await;
435
436 match &resp {
437 Ok(data) => {
438 tracing::info!("用户昵称: {}", data.name);
439 tracing::info!("用户等级: {}", data.level);
440 tracing::info!(
441 "是否为会员: {}",
442 data.vip.as_ref().is_some_and(|vip| vip.status > 0)
443 );
444 tracing::info!("是否有粉丝勋章: {}", data.fans_badge);
445 }
446 Err(e) => {
447 tracing::error!("请求失败: {:?}", e);
448 }
449 }
450
451 assert!(resp.is_ok());
452 tracing::info!("测试完成");
453 }
454
455 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
456 #[tokio::test]
457 async fn test_get_user_space_info_nonexistent() {
458 if !live_user_tests_enabled() {
459 return;
460 }
461
462 tracing::info!("开始测试获取不存在用户的空间详细信息");
463
464 let bpi = BpiClient::new().expect("client should build");
465 let mid = 0; tracing::info!("测试用户ID: {}", mid);
468
469 let resp = bpi
470 .user()
471 .space_info(UserSpaceParams::new(Mid::new(mid).expect("valid mid")))
472 .await;
473
474 match &resp {
475 Ok(data) => {
476 tracing::info!("意外返回用户: {}", data.name);
477 }
478 Err(e) => {
479 tracing::error!("请求失败: {:?}", e);
480 }
481 }
482
483 tracing::info!("测试完成");
484 }
485
486 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
487 #[tokio::test]
488 async fn test_get_user_card_info() {
489 if !live_user_tests_enabled() {
490 return;
491 }
492
493 tracing::info!("开始测试获取用户名片信息");
494
495 let bpi = BpiClient::new().expect("client should build");
496 let mid = 2; tracing::info!("测试用户ID: {}", mid);
499
500 let resp = bpi
501 .user()
502 .card(UserCardParams::new(Mid::new(mid).expect("valid mid")))
503 .await;
504
505 match &resp {
506 Ok(data) => {
507 tracing::info!("用户昵称: {}", data.card.name);
508 tracing::info!("用户性别: {:?}", data.card.sex);
509 tracing::info!("是否关注: {}", data.following);
510 tracing::info!("稿件数: {}", data.archive_count);
511 tracing::info!("粉丝数: {}", data.follower);
512 tracing::info!("点赞数: {}", data.like_num);
513 tracing::info!("用户签名: {}", data.card.sign);
514 }
515 Err(e) => {
516 tracing::error!("请求失败: {:?}", e);
517 }
518 }
519
520 assert!(resp.is_ok());
521 tracing::info!("测试完成");
522 }
523
524 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
525 #[tokio::test]
526 async fn test_get_user_card_info_with_photo() {
527 if !live_user_tests_enabled() {
528 return;
529 }
530
531 tracing::info!("开始测试获取用户名片信息(包含主页头图)");
532
533 let bpi = BpiClient::new().expect("client should build");
534 let mid = 2; tracing::info!("测试用户ID: {}", mid);
537
538 let resp = bpi
539 .user()
540 .card(
541 UserCardParams::new(Mid::new(mid).expect("valid mid"))
542 .with_photo(UserCardPhoto::Include),
543 )
544 .await;
545
546 match &resp {
547 Ok(data) => {
548 tracing::info!("用户昵称: {}", data.card.name);
549 tracing::info!("粉丝数: {}", data.card.fans);
550 }
551 Err(e) => {
552 tracing::error!("请求失败: {:?}", e);
553 }
554 }
555
556 assert!(resp.is_ok());
557 tracing::info!("测试完成");
558 }
559
560 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
561 #[tokio::test]
562 async fn test_get_user_card_info_without_photo() {
563 if !live_user_tests_enabled() {
564 return;
565 }
566
567 tracing::info!("开始测试获取用户名片信息(不包含主页头图)");
568
569 let bpi = BpiClient::new().expect("client should build");
570 let mid = 123456; tracing::info!("测试用户ID: {}", mid);
573
574 let resp = bpi
575 .user()
576 .card(
577 UserCardParams::new(Mid::new(mid).expect("valid mid"))
578 .with_photo(UserCardPhoto::Exclude),
579 )
580 .await;
581
582 match &resp {
583 Ok(data) => {
584 tracing::info!("用户昵称: {}", data.card.name);
585 tracing::info!("粉丝数: {}", data.card.fans);
586 tracing::info!("关注数: {}", data.card.attention);
587 }
588 Err(e) => {
589 tracing::error!("请求失败: {:?}", e);
590 }
591 }
592
593 assert!(resp.is_ok());
594 tracing::info!("测试完成");
595 }
596
597 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
598 #[tokio::test]
599 async fn test_get_user_card_info_invalid_user() {
600 if !live_user_tests_enabled() {
601 return;
602 }
603
604 tracing::info!("开始测试获取不存在用户的名片信息");
605
606 let bpi = BpiClient::new().expect("client should build");
607 let mid = 0; tracing::info!("测试用户ID: {}", mid);
610
611 let resp = bpi
612 .user()
613 .card(UserCardParams::new(Mid::new(mid).expect("valid mid")))
614 .await;
615
616 match &resp {
617 Ok(data) => {
618 tracing::info!("意外返回用户: {}", data.card.name);
619 }
620 Err(e) => {
621 tracing::error!("请求失败: {:?}", e);
622 }
623 }
624
625 tracing::info!("测试完成");
626 }
627
628 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
629 #[tokio::test]
630 async fn test_get_user_card_info_banned_user() {
631 if !live_user_tests_enabled() {
632 return;
633 }
634
635 tracing::info!("开始测试获取被封禁用户的名片信息");
636
637 let bpi = BpiClient::new().expect("client should build");
638 let mid = 999999999; tracing::info!("测试用户ID: {}", mid);
641
642 let resp = bpi
643 .user()
644 .card(UserCardParams::new(Mid::new(mid).expect("valid mid")))
645 .await;
646
647 match &resp {
648 Ok(data) => {
649 tracing::info!("请求成功,用户昵称: {}", data.card.name);
650 }
651 Err(e) => {
652 tracing::error!("请求失败: {:?}", e);
653 }
654 }
655
656 tracing::info!("测试完成");
657 }
658
659 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
660 #[tokio::test]
661 async fn test_user_cards_and_infos() {
662 if !live_user_tests_enabled() {
663 return;
664 }
665
666 let bpi = BpiClient::new().expect("client should build");
667
668 let cards = bpi
670 .user()
671 .cards(
672 UserCardsParams::new([
673 Mid::new(2).expect("valid mid"),
674 Mid::new(3).expect("valid mid"),
675 ])
676 .expect("valid params"),
677 )
678 .await
679 .unwrap();
680 tracing::info!("用户卡片: {:?}", cards);
681
682 let infos = bpi
684 .user()
685 .infos(
686 UserInfosParams::new([
687 Mid::new(2).expect("valid mid"),
688 Mid::new(3).expect("valid mid"),
689 ])
690 .expect("valid params"),
691 )
692 .await
693 .unwrap();
694 tracing::info!("用户详细信息: {:?}", infos);
695 }
696}