Skip to main content

bpi_rs/live/
guard.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Clone, Deserialize)]
4pub struct UserInfo {
5    /// 用户UID
6    pub uid: i64,
7    /// 用户基本信息
8    pub base: UserBaseInfo,
9    /// 粉丝牌信息
10    pub medal: UserMedalInfo,
11    /// 财富信息
12    pub wealth: Option<serde_json::Value>,
13    /// 标题
14    pub title: Option<serde_json::Value>,
15    /// 大航海信息
16    pub guard: UserGuardInfo,
17    /// 头像框
18    pub uhead_frame: Option<serde_json::Value>,
19    /// 大航海队长
20    pub guard_leader: Option<serde_json::Value>,
21}
22
23#[derive(Debug, Serialize, Clone, Deserialize)]
24pub struct GuardTabInfo {
25    /// 大航海总人数
26    pub num: i32,
27    /// 总页数
28    pub page: i32,
29    /// 当前页数
30    pub now: i32,
31    /// 成就等级
32    pub achievement_level: i32,
33    /// 主播守护成就等级
34    pub anchor_guard_achieve_level: i32,
35    /// 成就图标
36    pub achievement_icon_src: String,
37    /// 购买守护图标
38    pub buy_guard_icon_src: String,
39    /// 规则文档链接
40    pub rule_doc_src: String,
41    /// 背景图片
42    pub ex_background_src: String,
43    /// 颜色开始
44    pub color_start: String,
45    /// 颜色结束
46    pub color_end: String,
47    /// 标签颜色
48    pub tab_color: Vec<String>,
49    /// 标题颜色
50    pub title_color: Vec<String>,
51}
52
53#[derive(Debug, Serialize, Clone, Deserialize)]
54pub struct UserOriginInfo {
55    /// 用户名
56    pub name: String,
57    /// 头像
58    pub face: String,
59}
60
61#[derive(Debug, Serialize, Clone, Deserialize)]
62pub struct UserOfficialInfo {
63    /// 角色
64    pub role: i32,
65    /// 标题
66    pub title: String,
67    /// 描述
68    pub desc: String,
69    /// 类型
70    pub r#type: i32,
71}
72
73#[derive(Debug, Serialize, Clone, Deserialize)]
74pub struct UserBaseInfo {
75    /// 用户名
76    pub name: String,
77    /// 头像
78    pub face: String,
79    /// 名称颜色
80    pub name_color: i32,
81    /// 是否匿名
82    pub is_mystery: bool,
83    /// 风险控制信息
84    pub risk_ctrl_info: Option<serde_json::Value>,
85    /// 原始信息
86    pub origin_info: UserOriginInfo,
87    /// 官方信息
88    pub official_info: UserOfficialInfo,
89    /// 名称颜色字符串
90    pub name_color_str: String,
91}
92
93#[derive(Debug, Serialize, Clone, Deserialize)]
94pub struct UserMedalInfo {
95    /// 粉丝牌名称
96    pub name: String,
97    /// 粉丝牌等级
98    pub level: i32,
99    /// 颜色开始
100    pub color_start: i32,
101    /// 颜色结束
102    pub color_end: i32,
103    /// 边框颜色
104    pub color_border: i32,
105    /// 颜色
106    pub color: i32,
107    /// ID
108    pub id: i32,
109    /// 类型
110    pub typ: i32,
111    /// 是否点亮
112    pub is_light: i32,
113    /// 主播UID
114    pub ruid: i64,
115    /// 大航海等级
116    pub guard_level: i32,
117    /// 亲密度
118    pub score: i32,
119    /// 大航海图标
120    pub guard_icon: String,
121    /// 荣誉图标
122    pub honor_icon: String,
123    /// V2粉丝牌颜色开始
124    pub v2_medal_color_start: String,
125    /// V2粉丝牌颜色结束
126    pub v2_medal_color_end: String,
127    /// V2粉丝牌边框颜色
128    pub v2_medal_color_border: String,
129    /// V2粉丝牌文本颜色
130    pub v2_medal_color_text: String,
131    /// V2粉丝牌等级颜色
132    pub v2_medal_color_level: String,
133    /// 用户接收数量
134    pub user_receive_count: i32,
135}
136
137#[derive(Debug, Serialize, Clone, Deserialize)]
138pub struct UserGuardInfo {
139    /// 大航海等级
140    pub level: i32,
141    /// 过期时间字符串
142    pub expired_str: String,
143}
144
145#[derive(Debug, Serialize, Clone, Deserialize)]
146pub struct GuardMember {
147    /// 主播UID
148    pub ruid: i64,
149    /// 排名
150    pub rank: i32,
151    /// 陪伴天数
152    pub accompany: i32,
153    /// 用户信息
154    pub uinfo: UserInfo,
155    /// 亲密度
156    pub score: i32,
157}
158
159#[derive(Debug, Serialize, Clone, Deserialize)]
160pub struct GuardListData {
161    /// 大航海信息
162    pub info: GuardTabInfo,
163    /// 前三名
164    pub top3: Vec<GuardMember>,
165    /// 大航海成员列表
166    pub list: Vec<GuardMember>,
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::probe::contract::HttpMethod;
173    use crate::probe::endpoint_contract::EndpointContract;
174    use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
175
176    fn contract() -> BpiResult<EndpointContract> {
177        EndpointContract::from_slice(include_bytes!(
178            "../../tests/contracts/live/guard-read/guard-list/contract.json"
179        ))
180    }
181
182    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
183    #[tokio::test]
184    async fn test_get_guard_list() -> Result<(), Box<BpiError>> {
185        let bpi = BpiClient::new().expect("client should build");
186        let data = bpi
187            .live()
188            .guard_list(23174842, 504140200, None, None, None)
189            .await?;
190
191        assert!(!data.list.is_empty());
192        Ok(())
193    }
194
195    #[test]
196    fn live_guard_list_contract_matches_endpoint_request() -> BpiResult<()> {
197        let contract = contract()?;
198        let params: Vec<(&str, String)> = vec![
199            ("roomid", 23174842_i64.to_string()),
200            ("ruid", 504140200_i64.to_string()),
201            ("page", 1_i32.to_string()),
202            ("page_size", 20_i32.to_string()),
203            ("typ", 5_i32.to_string()),
204        ];
205
206        assert_eq!(contract.name, "live.guard_list");
207        assert_eq!(contract.request.method, HttpMethod::Get);
208        assert_eq!(
209            contract.request.url.as_str(),
210            "https://api.live.bilibili.com/xlive/app-room/v2/guardTab/topListNew"
211        );
212        assert_eq!(
213            contract.request.query.get("roomid").map(String::as_str),
214            Some("23174842")
215        );
216        assert_eq!(
217            contract.request.query.get("ruid").map(String::as_str),
218            Some("504140200")
219        );
220        assert_eq!(
221            params,
222            vec![
223                ("roomid", "23174842".to_string()),
224                ("ruid", "504140200".to_string()),
225                ("page", "1".to_string()),
226                ("page_size", "20".to_string()),
227                ("typ", "5".to_string()),
228            ]
229        );
230        assert_eq!(contract.cases.len(), 3);
231        assert_eq!(
232            contract.cases[0].response.rust_model.as_deref(),
233            Some("GuardListData")
234        );
235        Ok(())
236    }
237
238    #[test]
239    fn live_guard_list_response_fixture_parses_declared_model() -> BpiResult<()> {
240        let payload = ApiEnvelope::<GuardListData>::from_slice(include_bytes!(
241            "../../tests/contracts/live/guard-read/guard-list/responses/success.json"
242        ))?
243        .into_payload()?;
244
245        assert_eq!(payload.info.now, 1);
246        assert_eq!(payload.top3.len(), 1);
247        assert_eq!(payload.list.len(), 1);
248        Ok(())
249    }
250
251    fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
252        let path =
253            format!("target/bpi-probe-runs/live/guard-read/guard-list/{profile}.response.json");
254        let bytes = std::fs::read(path).ok()?;
255        let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
256        value
257            .get("response")
258            .and_then(|response| response.get("body"))
259            .cloned()
260    }
261
262    #[test]
263    fn live_guard_list_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
264        for profile in ["anonymous", "normal", "vip"] {
265            let Some(body) = local_probe_body(profile) else {
266                continue;
267            };
268            let payload =
269                serde_json::from_value::<ApiEnvelope<GuardListData>>(body)?.into_payload()?;
270
271            assert!(!payload.list.is_empty() || !payload.top3.is_empty());
272        }
273        Ok(())
274    }
275}