Skip to main content

bpi_rs/dynamic/
client.rs

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/// Dynamic API client.
38#[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    /// Gets the followed dynamic feed.
114    pub async fn all(&self, params: DynamicAllParams) -> BpiResult<DynamicAllData> {
115        self.client
116            .get(ALL_ENDPOINT)
117            .query(&params.query_pairs())
118            .send_bpi_payload("dynamic.feed_all")
119            .await
120    }
121
122    /// Checks whether the dynamic feed has new items.
123    pub async fn check_new(&self, params: DynamicCheckNewParams) -> BpiResult<DynamicUpdateData> {
124        self.client
125            .get(CHECK_NEW_ENDPOINT)
126            .query(&params.query_pairs())
127            .send_bpi_payload("dynamic.feed_all_update")
128            .await
129    }
130
131    /// Gets dynamic items shown in the navigation feed.
132    pub async fn nav_feed(&self, params: DynamicNavFeedParams) -> BpiResult<DynamicNavData> {
133        self.client
134            .get(NAV_FEED_ENDPOINT)
135            .query(&params.query_pairs())
136            .send_bpi_payload("dynamic.feed_nav")
137            .await
138    }
139
140    /// Gets the dynamic feed banner.
141    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    /// Gets a dynamic item detail.
154    pub async fn detail(&self, params: DynamicDetailParams) -> BpiResult<DynamicDetailData> {
155        self.client
156            .get(DETAIL_ENDPOINT)
157            .query(&params.query_pairs())
158            .send_bpi_payload("dynamic.detail")
159            .await
160    }
161
162    /// Gets reaction users for a dynamic item.
163    pub async fn reactions(
164        &self,
165        params: DynamicReactionsParams,
166    ) -> BpiResult<DynamicReactionData> {
167        self.client
168            .get(REACTIONS_ENDPOINT)
169            .query(&params.query_pairs())
170            .send_bpi_payload("dynamic.detail_reaction")
171            .await
172    }
173
174    /// Gets lottery notice detail for a dynamic item.
175    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(&params.query_pairs(&csrf))
184            .send_bpi_payload("dynamic.lottery_notice")
185            .await
186    }
187
188    /// Gets forwards for a dynamic item.
189    pub async fn forwards(&self, params: DynamicForwardsParams) -> BpiResult<DynamicForwardData> {
190        self.client
191            .get(FORWARDS_ENDPOINT)
192            .query(&params.query_pairs())
193            .send_bpi_payload("dynamic.detail_forward")
194            .await
195    }
196
197    /// Gets pictures for a dynamic item.
198    pub async fn pics(&self, params: DynamicPicsParams) -> BpiResult<Vec<DynamicPic>> {
199        self.client
200            .get(PICS_ENDPOINT)
201            .query(&params.query_pairs())
202            .send_bpi_payload("dynamic.detail_pic")
203            .await
204    }
205
206    /// Gets a forwarded dynamic item.
207    pub async fn forward_item(
208        &self,
209        params: DynamicForwardItemParams,
210    ) -> BpiResult<DynamicForwardInfoData> {
211        self.client
212            .get(FORWARD_ITEM_ENDPOINT)
213            .query(&params.query_pairs())
214            .send_bpi_payload("dynamic.detail_forward_item")
215            .await
216    }
217
218    /// Gets followed users who are currently live.
219    pub async fn live_users(&self, params: DynamicLiveUsersParams) -> BpiResult<LiveUsersData> {
220        self.client
221            .get(LIVE_USERS_ENDPOINT)
222            .query(&params.query_pairs())
223            .send_bpi_payload("dynamic.live_users")
224            .await
225    }
226
227    /// Gets followed users with new dynamic content.
228    pub async fn up_users(&self, params: DynamicUpUsersParams) -> BpiResult<DynUpUsersData> {
229        self.client
230            .get(UP_USERS_ENDPOINT)
231            .query(&params.query_pairs())
232            .send_bpi_payload("dynamic.up_users")
233            .await
234    }
235
236    /// Gets recently updated followed users.
237    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}