1use crate::dynamic::all::{DynamicAllData, DynamicUpdateData};
2use crate::dynamic::banner::DynamicBannerData;
3use crate::dynamic::content::{DynUpUsersData, LiveUsersData};
4use crate::dynamic::detail::{
5 DynamicDetailData, DynamicForwardData, DynamicForwardInfoData, DynamicLotteryData, DynamicPic,
6 DynamicReactionData,
7};
8use crate::dynamic::get_dynamic_detail::RecentUpData;
9use crate::dynamic::nav::DynamicNavData;
10use crate::dynamic::{
11 DynamicAllParams, DynamicCheckNewParams, DynamicDetailParams, DynamicForwardItemParams,
12 DynamicForwardsParams, DynamicLiveUsersParams, DynamicLotteryNoticeParams,
13 DynamicNavFeedParams, DynamicPicsParams, DynamicReactionsParams, DynamicUpUsersParams,
14};
15use crate::{BilibiliRequest, BpiClient, BpiResult};
16
17const ALL_ENDPOINT: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all";
18const CHECK_NEW_ENDPOINT: &str =
19 "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all/update";
20const NAV_FEED_ENDPOINT: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/nav";
21const FEED_BANNER_ENDPOINT: &str = "https://api.bilibili.com/x/dynamic/feed/dyn/banner";
22const DETAIL_ENDPOINT: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail";
23const REACTIONS_ENDPOINT: &str =
24 "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/reaction";
25const LOTTERY_NOTICE_ENDPOINT: &str =
26 "https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice";
27const FORWARDS_ENDPOINT: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/forward";
28const PICS_ENDPOINT: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/pic";
29const FORWARD_ITEM_ENDPOINT: &str =
30 "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/forward/item";
31const LIVE_USERS_ENDPOINT: &str =
32 "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/w_live_users";
33const UP_USERS_ENDPOINT: &str =
34 "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/w_dyn_uplist";
35const RECENT_UP_ENDPOINT: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/portal";
36
37#[derive(Clone, Copy)]
39pub struct DynamicClient<'a> {
40 pub(crate) client: &'a BpiClient,
41}
42
43impl<'a> DynamicClient<'a> {
44 pub(crate) fn new(client: &'a BpiClient) -> Self {
45 Self { client }
46 }
47
48 #[cfg(test)]
49 pub(crate) fn all_endpoint(&self) -> &'static str {
50 ALL_ENDPOINT
51 }
52
53 #[cfg(test)]
54 pub(crate) fn check_new_endpoint(&self) -> &'static str {
55 CHECK_NEW_ENDPOINT
56 }
57
58 #[cfg(test)]
59 pub(crate) fn nav_feed_endpoint(&self) -> &'static str {
60 NAV_FEED_ENDPOINT
61 }
62
63 #[cfg(test)]
64 pub(crate) fn feed_banner_endpoint(&self) -> &'static str {
65 FEED_BANNER_ENDPOINT
66 }
67
68 #[cfg(test)]
69 pub(crate) fn detail_endpoint(&self) -> &'static str {
70 DETAIL_ENDPOINT
71 }
72
73 #[cfg(test)]
74 pub(crate) fn reactions_endpoint(&self) -> &'static str {
75 REACTIONS_ENDPOINT
76 }
77
78 #[cfg(test)]
79 pub(crate) fn lottery_notice_endpoint(&self) -> &'static str {
80 LOTTERY_NOTICE_ENDPOINT
81 }
82
83 #[cfg(test)]
84 pub(crate) fn forwards_endpoint(&self) -> &'static str {
85 FORWARDS_ENDPOINT
86 }
87
88 #[cfg(test)]
89 pub(crate) fn pics_endpoint(&self) -> &'static str {
90 PICS_ENDPOINT
91 }
92
93 #[cfg(test)]
94 pub(crate) fn forward_item_endpoint(&self) -> &'static str {
95 FORWARD_ITEM_ENDPOINT
96 }
97
98 #[cfg(test)]
99 pub(crate) fn live_users_endpoint(&self) -> &'static str {
100 LIVE_USERS_ENDPOINT
101 }
102
103 #[cfg(test)]
104 pub(crate) fn up_users_endpoint(&self) -> &'static str {
105 UP_USERS_ENDPOINT
106 }
107
108 #[cfg(test)]
109 pub(crate) fn recent_up_endpoint(&self) -> &'static str {
110 RECENT_UP_ENDPOINT
111 }
112
113 pub async fn all(&self, params: DynamicAllParams) -> BpiResult<DynamicAllData> {
115 self.client
116 .get(ALL_ENDPOINT)
117 .query(¶ms.query_pairs())
118 .send_bpi_payload("dynamic.feed_all")
119 .await
120 }
121
122 pub async fn check_new(&self, params: DynamicCheckNewParams) -> BpiResult<DynamicUpdateData> {
124 self.client
125 .get(CHECK_NEW_ENDPOINT)
126 .query(¶ms.query_pairs())
127 .send_bpi_payload("dynamic.feed_all_update")
128 .await
129 }
130
131 pub async fn nav_feed(&self, params: DynamicNavFeedParams) -> BpiResult<DynamicNavData> {
133 self.client
134 .get(NAV_FEED_ENDPOINT)
135 .query(¶ms.query_pairs())
136 .send_bpi_payload("dynamic.feed_nav")
137 .await
138 }
139
140 pub async fn feed_banner(&self) -> BpiResult<DynamicBannerData> {
142 self.client
143 .get(FEED_BANNER_ENDPOINT)
144 .query(&[
145 ("platform", "1"),
146 ("position", "web动态"),
147 ("web_location", "333.1365"),
148 ])
149 .send_bpi_payload("dynamic.feed_banner")
150 .await
151 }
152
153 pub async fn detail(&self, params: DynamicDetailParams) -> BpiResult<DynamicDetailData> {
155 self.client
156 .get(DETAIL_ENDPOINT)
157 .query(¶ms.query_pairs())
158 .send_bpi_payload("dynamic.detail")
159 .await
160 }
161
162 pub async fn reactions(
164 &self,
165 params: DynamicReactionsParams,
166 ) -> BpiResult<DynamicReactionData> {
167 self.client
168 .get(REACTIONS_ENDPOINT)
169 .query(¶ms.query_pairs())
170 .send_bpi_payload("dynamic.detail_reaction")
171 .await
172 }
173
174 pub async fn lottery_notice(
176 &self,
177 params: DynamicLotteryNoticeParams,
178 ) -> BpiResult<DynamicLotteryData> {
179 let csrf = self.client.csrf()?;
180
181 self.client
182 .get(LOTTERY_NOTICE_ENDPOINT)
183 .query(¶ms.query_pairs(&csrf))
184 .send_bpi_payload("dynamic.lottery_notice")
185 .await
186 }
187
188 pub async fn forwards(&self, params: DynamicForwardsParams) -> BpiResult<DynamicForwardData> {
190 self.client
191 .get(FORWARDS_ENDPOINT)
192 .query(¶ms.query_pairs())
193 .send_bpi_payload("dynamic.detail_forward")
194 .await
195 }
196
197 pub async fn pics(&self, params: DynamicPicsParams) -> BpiResult<Vec<DynamicPic>> {
199 self.client
200 .get(PICS_ENDPOINT)
201 .query(¶ms.query_pairs())
202 .send_bpi_payload("dynamic.detail_pic")
203 .await
204 }
205
206 pub async fn forward_item(
208 &self,
209 params: DynamicForwardItemParams,
210 ) -> BpiResult<DynamicForwardInfoData> {
211 self.client
212 .get(FORWARD_ITEM_ENDPOINT)
213 .query(¶ms.query_pairs())
214 .send_bpi_payload("dynamic.detail_forward_item")
215 .await
216 }
217
218 pub async fn live_users(&self, params: DynamicLiveUsersParams) -> BpiResult<LiveUsersData> {
220 self.client
221 .get(LIVE_USERS_ENDPOINT)
222 .query(¶ms.query_pairs())
223 .send_bpi_payload("dynamic.live_users")
224 .await
225 }
226
227 pub async fn up_users(&self, params: DynamicUpUsersParams) -> BpiResult<DynUpUsersData> {
229 self.client
230 .get(UP_USERS_ENDPOINT)
231 .query(¶ms.query_pairs())
232 .send_bpi_payload("dynamic.up_users")
233 .await
234 }
235
236 pub async fn recent_up(&self) -> BpiResult<RecentUpData> {
238 self.client
239 .get(RECENT_UP_ENDPOINT)
240 .send_bpi_payload("dynamic.recent_up")
241 .await
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use std::future::Future;
248
249 use crate::dynamic::all::{DynamicAllData, DynamicUpdateData};
250 use crate::dynamic::banner::DynamicBannerData;
251 use crate::dynamic::content::{DynUpUsersData, LiveUsersData};
252 use crate::dynamic::detail::{
253 DynamicDetailData, DynamicForwardData, DynamicForwardInfoData, DynamicLotteryData,
254 DynamicPic, DynamicReactionData,
255 };
256 use crate::dynamic::get_dynamic_detail::RecentUpData;
257 use crate::dynamic::nav::DynamicNavData;
258 use crate::dynamic::{
259 DynamicAllParams, DynamicCheckNewParams, DynamicDetailParams, DynamicForwardItemParams,
260 DynamicForwardsParams, DynamicLiveUsersParams, DynamicLotteryNoticeParams,
261 DynamicNavFeedParams, DynamicPicsParams, DynamicReactionsParams, DynamicUpUsersParams,
262 };
263 use crate::ids::DynamicId;
264 use crate::probe::contract::HttpMethod;
265 use crate::probe::endpoint_contract::EndpointContract;
266 use crate::{BpiClient, BpiError, BpiResult};
267
268 fn dynamic_id(value: &str) -> Result<DynamicId, BpiError> {
269 value.parse()
270 }
271
272 fn assert_all_future<F>(_future: F)
273 where
274 F: Future<Output = BpiResult<DynamicAllData>>,
275 {
276 }
277
278 fn assert_check_new_future<F>(_future: F)
279 where
280 F: Future<Output = BpiResult<DynamicUpdateData>>,
281 {
282 }
283
284 fn assert_nav_feed_future<F>(_future: F)
285 where
286 F: Future<Output = BpiResult<DynamicNavData>>,
287 {
288 }
289
290 fn assert_feed_banner_future<F>(_future: F)
291 where
292 F: Future<Output = BpiResult<DynamicBannerData>>,
293 {
294 }
295
296 fn assert_detail_future<F>(_future: F)
297 where
298 F: Future<Output = BpiResult<DynamicDetailData>>,
299 {
300 }
301
302 fn assert_reactions_future<F>(_future: F)
303 where
304 F: Future<Output = BpiResult<DynamicReactionData>>,
305 {
306 }
307
308 fn assert_lottery_notice_future<F>(_future: F)
309 where
310 F: Future<Output = BpiResult<DynamicLotteryData>>,
311 {
312 }
313
314 fn assert_forwards_future<F>(_future: F)
315 where
316 F: Future<Output = BpiResult<DynamicForwardData>>,
317 {
318 }
319
320 fn assert_pics_future<F>(_future: F)
321 where
322 F: Future<Output = BpiResult<Vec<DynamicPic>>>,
323 {
324 }
325
326 fn assert_forward_item_future<F>(_future: F)
327 where
328 F: Future<Output = BpiResult<DynamicForwardInfoData>>,
329 {
330 }
331
332 fn assert_live_users_future<F>(_future: F)
333 where
334 F: Future<Output = BpiResult<LiveUsersData>>,
335 {
336 }
337
338 fn assert_up_users_future<F>(_future: F)
339 where
340 F: Future<Output = BpiResult<DynUpUsersData>>,
341 {
342 }
343
344 fn assert_recent_up_future<F>(_future: F)
345 where
346 F: Future<Output = BpiResult<RecentUpData>>,
347 {
348 }
349
350 fn contract(path: &str) -> BpiResult<EndpointContract> {
351 let bytes = match path {
352 "feed/all" => {
353 include_bytes!("../../tests/contracts/dynamic/feed/all/contract.json").as_slice()
354 }
355 "feed/check-new" => {
356 include_bytes!("../../tests/contracts/dynamic/feed/check-new/contract.json")
357 .as_slice()
358 }
359 "feed/nav" => {
360 include_bytes!("../../tests/contracts/dynamic/feed/nav/contract.json").as_slice()
361 }
362 "feed/banner" => {
363 include_bytes!("../../tests/contracts/dynamic/feed/banner/contract.json").as_slice()
364 }
365 "detail/detail" => {
366 include_bytes!("../../tests/contracts/dynamic/detail/detail/contract.json")
367 .as_slice()
368 }
369 "detail/reactions" => {
370 include_bytes!("../../tests/contracts/dynamic/detail/reactions/contract.json")
371 .as_slice()
372 }
373 "detail/forwards" => {
374 include_bytes!("../../tests/contracts/dynamic/detail/forwards/contract.json")
375 .as_slice()
376 }
377 "detail/pics" => {
378 include_bytes!("../../tests/contracts/dynamic/detail/pics/contract.json").as_slice()
379 }
380 "detail/forward-item" => {
381 include_bytes!("../../tests/contracts/dynamic/detail/forward-item/contract.json")
382 .as_slice()
383 }
384 "content/live-users" => {
385 include_bytes!("../../tests/contracts/dynamic/content/live-users/contract.json")
386 .as_slice()
387 }
388 "content/up-users" => {
389 include_bytes!("../../tests/contracts/dynamic/content/up-users/contract.json")
390 .as_slice()
391 }
392 "content/recent-up" => {
393 include_bytes!("../../tests/contracts/dynamic/content/recent-up/contract.json")
394 .as_slice()
395 }
396 "lottery-notice-read/lottery-notice" => include_bytes!(
397 "../../tests/contracts/dynamic/lottery-notice-read/lottery-notice/contract.json"
398 )
399 .as_slice(),
400 _ => unreachable!("unknown dynamic contract"),
401 };
402 EndpointContract::from_slice(bytes)
403 }
404
405 #[test]
406 fn dynamic_methods_return_payload_futures() -> BpiResult<()> {
407 let client = BpiClient::new()?;
408 let dynamic = client.dynamic();
409 let detail_id = dynamic_id("1099138163191840776")?;
410 let forward_item_id = dynamic_id("1110902525317349376")?;
411 let lottery_id = dynamic_id("969916293954142214")?;
412
413 assert_all_future(dynamic.all(DynamicAllParams::new()));
414 assert_check_new_future(dynamic.check_new(DynamicCheckNewParams::new("0")?));
415 assert_nav_feed_future(dynamic.nav_feed(DynamicNavFeedParams::new()));
416 assert_feed_banner_future(dynamic.feed_banner());
417 assert_detail_future(dynamic.detail(DynamicDetailParams::new(detail_id.clone())));
418 assert_reactions_future(dynamic.reactions(DynamicReactionsParams::new(detail_id.clone())));
419 assert_lottery_notice_future(
420 dynamic.lottery_notice(DynamicLotteryNoticeParams::new(lottery_id)),
421 );
422 assert_forwards_future(dynamic.forwards(DynamicForwardsParams::new(detail_id.clone())));
423 assert_pics_future(dynamic.pics(DynamicPicsParams::new(detail_id)));
424 assert_forward_item_future(
425 dynamic.forward_item(DynamicForwardItemParams::new(forward_item_id)),
426 );
427 assert_live_users_future(dynamic.live_users(DynamicLiveUsersParams::new().with_size(1)?));
428 assert_up_users_future(dynamic.up_users(DynamicUpUsersParams::new()));
429 assert_recent_up_future(dynamic.recent_up());
430 Ok(())
431 }
432
433 #[test]
434 fn dynamic_contracts_match_module_client_endpoints() -> BpiResult<()> {
435 let client = BpiClient::new()?;
436 let dynamic = client.dynamic();
437
438 let cases = [
439 ("feed/all", "dynamic.feed_all", dynamic.all_endpoint()),
440 (
441 "feed/check-new",
442 "dynamic.feed_all_update",
443 dynamic.check_new_endpoint(),
444 ),
445 ("feed/nav", "dynamic.feed_nav", dynamic.nav_feed_endpoint()),
446 (
447 "feed/banner",
448 "dynamic.feed_banner",
449 dynamic.feed_banner_endpoint(),
450 ),
451 ("detail/detail", "dynamic.detail", dynamic.detail_endpoint()),
452 (
453 "detail/reactions",
454 "dynamic.detail_reaction",
455 dynamic.reactions_endpoint(),
456 ),
457 (
458 "detail/forwards",
459 "dynamic.detail_forward",
460 dynamic.forwards_endpoint(),
461 ),
462 ("detail/pics", "dynamic.detail_pic", dynamic.pics_endpoint()),
463 (
464 "detail/forward-item",
465 "dynamic.detail_forward_item",
466 dynamic.forward_item_endpoint(),
467 ),
468 (
469 "content/live-users",
470 "dynamic.live_users",
471 dynamic.live_users_endpoint(),
472 ),
473 (
474 "content/up-users",
475 "dynamic.up_users",
476 dynamic.up_users_endpoint(),
477 ),
478 (
479 "content/recent-up",
480 "dynamic.recent_up",
481 dynamic.recent_up_endpoint(),
482 ),
483 (
484 "lottery-notice-read/lottery-notice",
485 "dynamic.lottery_notice",
486 dynamic.lottery_notice_endpoint(),
487 ),
488 ];
489
490 for (path, name, endpoint) in cases {
491 let contract = contract(path)?;
492 assert_eq!(contract.name, name);
493 assert_eq!(contract.request.method, HttpMethod::Get);
494 assert_eq!(contract.request.url.as_str(), endpoint);
495 }
496 Ok(())
497 }
498}