Skip to main content

bpi_rs/electric/
client.rs

1use chrono::NaiveDate;
2
3use crate::electric::charge_list::{
4    ChargeMonthUpData, ElecRankData, RechargeData, VideoElecShowData,
5};
6use crate::electric::charge_msg::{ElecRemarkDetail, ElecRemarkList};
7use crate::electric::monthly::{
8    ChargeFollowInfo, ChargeRecordData, MemberRankData, UpowerItemDetail,
9};
10use crate::{BilibiliRequest, BpiClient, BpiResult};
11
12const MONTH_UP_LIST_ENDPOINT: &str = "https://api.bilibili.com/x/ugcpay-rank/elec/month/up";
13const VIDEO_SHOW_ENDPOINT: &str = "https://api.bilibili.com/x/web-interface/elec/show";
14const RECHARGE_LIST_ENDPOINT: &str =
15    "https://pay.bilibili.com/bk/brokerage/listForCustomerRechargeRecord";
16const RANK_RECENT_ENDPOINT: &str = "https://member.bilibili.com/x/h5/elec/rank/recent";
17const CHARGE_RECORD_ENDPOINT: &str =
18    "https://api.live.bilibili.com/xlive/revenue/v1/guard/getChargeRecord";
19const UPOWER_ITEM_DETAIL_ENDPOINT: &str = "https://api.bilibili.com/x/upower/item/detail";
20const CHARGE_FOLLOW_INFO_ENDPOINT: &str = "https://api.bilibili.com/x/upower/charge/follow/info";
21const UPOWER_MEMBER_RANK_ENDPOINT: &str = "https://api.bilibili.com/x/upower/up/member/rank/v2";
22const REMARK_LIST_ENDPOINT: &str = "https://member.bilibili.com/x/web/elec/remark/list";
23const REMARK_DETAIL_ENDPOINT: &str = "https://member.bilibili.com/x/web/elec/remark/detail";
24
25/// Electric charging API client.
26#[derive(Clone, Copy)]
27pub struct ElectricClient<'a> {
28    pub(crate) client: &'a BpiClient,
29}
30
31impl<'a> ElectricClient<'a> {
32    pub(crate) fn new(client: &'a BpiClient) -> Self {
33        Self { client }
34    }
35
36    #[cfg(test)]
37    pub(crate) fn month_up_list_endpoint(&self) -> &'static str {
38        MONTH_UP_LIST_ENDPOINT
39    }
40
41    #[cfg(test)]
42    pub(crate) fn video_show_endpoint(&self) -> &'static str {
43        VIDEO_SHOW_ENDPOINT
44    }
45
46    #[cfg(test)]
47    pub(crate) fn recharge_list_endpoint(&self) -> &'static str {
48        RECHARGE_LIST_ENDPOINT
49    }
50
51    #[cfg(test)]
52    pub(crate) fn rank_recent_endpoint(&self) -> &'static str {
53        RANK_RECENT_ENDPOINT
54    }
55
56    #[cfg(test)]
57    pub(crate) fn charge_record_endpoint(&self) -> &'static str {
58        CHARGE_RECORD_ENDPOINT
59    }
60
61    #[cfg(test)]
62    pub(crate) fn upower_item_detail_endpoint(&self) -> &'static str {
63        UPOWER_ITEM_DETAIL_ENDPOINT
64    }
65
66    #[cfg(test)]
67    pub(crate) fn charge_follow_info_endpoint(&self) -> &'static str {
68        CHARGE_FOLLOW_INFO_ENDPOINT
69    }
70
71    #[cfg(test)]
72    pub(crate) fn upower_member_rank_endpoint(&self) -> &'static str {
73        UPOWER_MEMBER_RANK_ENDPOINT
74    }
75
76    #[cfg(test)]
77    pub(crate) fn remark_list_endpoint(&self) -> &'static str {
78        REMARK_LIST_ENDPOINT
79    }
80
81    #[cfg(test)]
82    pub(crate) fn remark_detail_endpoint(&self) -> &'static str {
83        REMARK_DETAIL_ENDPOINT
84    }
85
86    /// Gets the monthly charging public list for an UP user.
87    pub async fn month_up_list(&self, up_mid: i64) -> BpiResult<ChargeMonthUpData> {
88        self.client
89            .get(MONTH_UP_LIST_ENDPOINT)
90            .query(&[("up_mid", up_mid)])
91            .send_bpi_payload("electric.month_up_list")
92            .await
93    }
94
95    /// Gets video charging acknowledgements.
96    pub async fn video_show(
97        &self,
98        mid: i64,
99        aid: Option<i64>,
100        bvid: Option<&str>,
101    ) -> BpiResult<VideoElecShowData> {
102        let mut request = self.client.get(VIDEO_SHOW_ENDPOINT).query(&[("mid", mid)]);
103
104        if let Some(aid) = aid {
105            request = request.query(&[("aid", aid)]);
106        }
107        if let Some(bvid) = bvid {
108            request = request.query(&[("bvid", bvid)]);
109        }
110
111        request.send_bpi_payload("electric.video_show").await
112    }
113
114    /// Gets the authenticated account's received charging records.
115    pub async fn recharge_list(
116        &self,
117        page: u64,
118        page_size: u64,
119        begin_time: Option<NaiveDate>,
120        end_time: Option<NaiveDate>,
121    ) -> BpiResult<RechargeData> {
122        let mut request = self
123            .client
124            .get(RECHARGE_LIST_ENDPOINT)
125            .query(&[("customerId", "10026")])
126            .query(&[("currentPage", page), ("pageSize", page_size)]);
127
128        if let Some(begin_time) = begin_time {
129            request = request.query(&[("beginTime", begin_time.format("%Y-%m-%d").to_string())]);
130        }
131        if let Some(end_time) = end_time {
132            request = request.query(&[("endTime", end_time.format("%Y-%m-%d").to_string())]);
133        }
134
135        request.send_bpi_payload("electric.recharge_list").await
136    }
137
138    /// Gets recent charging rank history.
139    pub async fn rank_recent(&self, pn: Option<u64>, ps: Option<u64>) -> BpiResult<ElecRankData> {
140        let mut request = self.client.get(RANK_RECENT_ENDPOINT);
141
142        if let Some(pn) = pn {
143            request = request.query(&[("pn", pn)]);
144        }
145        if let Some(ps) = ps {
146            request = request.query(&[("ps", ps)]);
147        }
148
149        request.send_bpi_payload("electric.rank_recent").await
150    }
151
152    /// Gets the authenticated account's monthly charging records.
153    pub async fn charge_record(&self, page: u64, charge_type: u32) -> BpiResult<ChargeRecordData> {
154        self.client
155            .get(CHARGE_RECORD_ENDPOINT)
156            .query(&[("page", page)])
157            .query(&[("type", charge_type)])
158            .send_bpi_payload("electric.charge_record")
159            .await
160    }
161
162    /// Gets monthly charging item details for an UP user.
163    pub async fn upower_item_detail(&self, up_mid: u64) -> BpiResult<UpowerItemDetail> {
164        self.client
165            .get(UPOWER_ITEM_DETAIL_ENDPOINT)
166            .query(&[("up_mid", up_mid)])
167            .send_bpi_payload("electric.upower_item_detail")
168            .await
169    }
170
171    /// Gets the authenticated account's monthly charging relationship with an UP user.
172    pub async fn charge_follow_info(&self, up_mid: u64) -> BpiResult<ChargeFollowInfo> {
173        self.client
174            .get(CHARGE_FOLLOW_INFO_ENDPOINT)
175            .query(&[("up_mid", up_mid)])
176            .send_bpi_payload("electric.charge_follow_info")
177            .await
178    }
179
180    /// Gets the monthly charging member rank for an UP user.
181    pub async fn upower_member_rank(
182        &self,
183        up_mid: u64,
184        pn: u64,
185        ps: u64,
186        privilege_type: Option<u64>,
187    ) -> BpiResult<MemberRankData> {
188        let mut request = self.client.get(UPOWER_MEMBER_RANK_ENDPOINT).query(&[
189            ("up_mid", up_mid),
190            ("pn", pn),
191            ("ps", ps),
192        ]);
193
194        if let Some(privilege_type) = privilege_type {
195            request = request.query(&[("privilege_type", privilege_type)]);
196        }
197
198        request
199            .send_bpi_payload("electric.upower_member_rank")
200            .await
201    }
202
203    /// Lists charging remarks received by the authenticated account.
204    pub async fn remark_list(
205        &self,
206        pn: Option<u64>,
207        ps: Option<u64>,
208        begin: Option<NaiveDate>,
209        end: Option<NaiveDate>,
210    ) -> BpiResult<ElecRemarkList> {
211        let mut request = self.client.get(REMARK_LIST_ENDPOINT);
212
213        if let Some(pn) = pn {
214            request = request.query(&[("pn", pn)]);
215        }
216        if let Some(ps) = ps {
217            request = request.query(&[("ps", ps)]);
218        }
219        if let Some(begin) = begin {
220            request = request.query(&[("begin", begin.format("%Y-%m-%d").to_string())]);
221        }
222        if let Some(end) = end {
223            request = request.query(&[("end", end.format("%Y-%m-%d").to_string())]);
224        }
225
226        request.send_bpi_payload("electric.remark_list").await
227    }
228
229    /// Gets one charging remark by id.
230    pub async fn remark_detail(&self, id: u64) -> BpiResult<ElecRemarkDetail> {
231        self.client
232            .get(REMARK_DETAIL_ENDPOINT)
233            .query(&[("id", id)])
234            .send_bpi_payload("electric.remark_detail")
235            .await
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use std::future::Future;
242
243    use crate::electric::charge_list::{
244        ChargeMonthUpData, ElecRankData, RechargeData, VideoElecShowData,
245    };
246    use crate::electric::charge_msg::{ElecRemarkDetail, ElecRemarkList};
247    use crate::electric::monthly::{
248        ChargeFollowInfo, ChargeRecordData, MemberRankData, UpowerItemDetail,
249    };
250    use crate::probe::contract::HttpMethod;
251    use crate::probe::endpoint_contract::EndpointContract;
252    use crate::{BpiClient, BpiResult};
253
254    fn assert_month_up_list_future<F>(_future: F)
255    where
256        F: Future<Output = BpiResult<ChargeMonthUpData>>,
257    {
258    }
259
260    fn assert_video_show_future<F>(_future: F)
261    where
262        F: Future<Output = BpiResult<VideoElecShowData>>,
263    {
264    }
265
266    fn assert_recharge_list_future<F>(_future: F)
267    where
268        F: Future<Output = BpiResult<RechargeData>>,
269    {
270    }
271
272    fn assert_rank_recent_future<F>(_future: F)
273    where
274        F: Future<Output = BpiResult<ElecRankData>>,
275    {
276    }
277
278    fn assert_charge_record_future<F>(_future: F)
279    where
280        F: Future<Output = BpiResult<ChargeRecordData>>,
281    {
282    }
283
284    fn assert_upower_item_detail_future<F>(_future: F)
285    where
286        F: Future<Output = BpiResult<UpowerItemDetail>>,
287    {
288    }
289
290    fn assert_charge_follow_info_future<F>(_future: F)
291    where
292        F: Future<Output = BpiResult<ChargeFollowInfo>>,
293    {
294    }
295
296    fn assert_upower_member_rank_future<F>(_future: F)
297    where
298        F: Future<Output = BpiResult<MemberRankData>>,
299    {
300    }
301
302    fn assert_remark_list_future<F>(_future: F)
303    where
304        F: Future<Output = BpiResult<ElecRemarkList>>,
305    {
306    }
307
308    fn assert_remark_detail_future<F>(_future: F)
309    where
310        F: Future<Output = BpiResult<ElecRemarkDetail>>,
311    {
312    }
313
314    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
315        let bytes = match endpoint {
316            "month-up-list" => include_bytes!(
317                "../../tests/contracts/electric/public-read/month-up-list/contract.json"
318            )
319            .as_slice(),
320            "video-show" => include_bytes!(
321                "../../tests/contracts/electric/public-read/video-show/contract.json"
322            )
323            .as_slice(),
324            "upower-item-detail" => include_bytes!(
325                "../../tests/contracts/electric/public-read/upower-item-detail/contract.json"
326            )
327            .as_slice(),
328            "upower-member-rank" => include_bytes!(
329                "../../tests/contracts/electric/public-read/upower-member-rank/contract.json"
330            )
331            .as_slice(),
332            "recharge-list" => include_bytes!(
333                "../../tests/contracts/electric/private-read/recharge-list/contract.json"
334            )
335            .as_slice(),
336            "rank-recent" => include_bytes!(
337                "../../tests/contracts/electric/private-read/rank-recent/contract.json"
338            )
339            .as_slice(),
340            "charge-record" => include_bytes!(
341                "../../tests/contracts/electric/private-read/charge-record/contract.json"
342            )
343            .as_slice(),
344            "charge-follow-info" => include_bytes!(
345                "../../tests/contracts/electric/private-read/charge-follow-info/contract.json"
346            )
347            .as_slice(),
348            "remark-list" => include_bytes!(
349                "../../tests/contracts/electric/private-read/remark-list/contract.json"
350            )
351            .as_slice(),
352            "remark-detail" => include_bytes!(
353                "../../tests/contracts/electric/private-read/remark-detail/contract.json"
354            )
355            .as_slice(),
356            _ => unreachable!("unknown electric contract"),
357        };
358
359        EndpointContract::from_slice(bytes)
360    }
361
362    #[test]
363    fn electric_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
364        let client = BpiClient::new()?;
365        let electric = client.electric();
366
367        assert_eq!(
368            electric.month_up_list_endpoint(),
369            "https://api.bilibili.com/x/ugcpay-rank/elec/month/up"
370        );
371        assert_eq!(
372            electric.video_show_endpoint(),
373            "https://api.bilibili.com/x/web-interface/elec/show"
374        );
375        assert_eq!(
376            electric.recharge_list_endpoint(),
377            "https://pay.bilibili.com/bk/brokerage/listForCustomerRechargeRecord"
378        );
379        assert_eq!(
380            electric.rank_recent_endpoint(),
381            "https://member.bilibili.com/x/h5/elec/rank/recent"
382        );
383        assert_eq!(
384            electric.charge_record_endpoint(),
385            "https://api.live.bilibili.com/xlive/revenue/v1/guard/getChargeRecord"
386        );
387        assert_eq!(
388            electric.upower_item_detail_endpoint(),
389            "https://api.bilibili.com/x/upower/item/detail"
390        );
391        assert_eq!(
392            electric.charge_follow_info_endpoint(),
393            "https://api.bilibili.com/x/upower/charge/follow/info"
394        );
395        assert_eq!(
396            electric.upower_member_rank_endpoint(),
397            "https://api.bilibili.com/x/upower/up/member/rank/v2"
398        );
399        assert_eq!(
400            electric.remark_list_endpoint(),
401            "https://member.bilibili.com/x/web/elec/remark/list"
402        );
403        assert_eq!(
404            electric.remark_detail_endpoint(),
405            "https://member.bilibili.com/x/web/elec/remark/detail"
406        );
407        Ok(())
408    }
409
410    #[test]
411    fn electric_methods_return_payload_futures() {
412        let client = BpiClient::new().expect("client should build");
413        let electric = client.electric();
414
415        assert_month_up_list_future(electric.month_up_list(53456));
416        assert_video_show_future(electric.video_show(53456, None, Some("BV1Dh411S7sS")));
417        assert_recharge_list_future(electric.recharge_list(1, 10, None, None));
418        assert_rank_recent_future(electric.rank_recent(Some(1), Some(10)));
419        assert_charge_record_future(electric.charge_record(1, 1));
420        assert_upower_item_detail_future(electric.upower_item_detail(1265680561));
421        assert_charge_follow_info_future(electric.charge_follow_info(1265680561));
422        assert_upower_member_rank_future(electric.upower_member_rank(1265680561, 1, 10, None));
423        assert_remark_list_future(electric.remark_list(Some(1), Some(10), None, None));
424        assert_remark_detail_future(electric.remark_detail(1));
425    }
426
427    #[test]
428    fn electric_contracts_match_module_client_endpoints() -> BpiResult<()> {
429        let client = BpiClient::new()?;
430        let electric = client.electric();
431
432        let expectations = [
433            (
434                "month-up-list",
435                "electric.month_up_list",
436                electric.month_up_list_endpoint(),
437            ),
438            (
439                "video-show",
440                "electric.video_show",
441                electric.video_show_endpoint(),
442            ),
443            (
444                "upower-item-detail",
445                "electric.upower_item_detail",
446                electric.upower_item_detail_endpoint(),
447            ),
448            (
449                "upower-member-rank",
450                "electric.upower_member_rank",
451                electric.upower_member_rank_endpoint(),
452            ),
453            (
454                "recharge-list",
455                "electric.recharge_list",
456                electric.recharge_list_endpoint(),
457            ),
458            (
459                "rank-recent",
460                "electric.rank_recent",
461                electric.rank_recent_endpoint(),
462            ),
463            (
464                "charge-record",
465                "electric.charge_record",
466                electric.charge_record_endpoint(),
467            ),
468            (
469                "charge-follow-info",
470                "electric.charge_follow_info",
471                electric.charge_follow_info_endpoint(),
472            ),
473            (
474                "remark-list",
475                "electric.remark_list",
476                electric.remark_list_endpoint(),
477            ),
478            (
479                "remark-detail",
480                "electric.remark_detail",
481                electric.remark_detail_endpoint(),
482            ),
483        ];
484
485        for (endpoint, name, url) in expectations {
486            let contract = contract(endpoint)?;
487
488            assert_eq!(contract.name, name);
489            assert_eq!(contract.request.method, HttpMethod::Get);
490            assert_eq!(contract.request.url.as_str(), url);
491        }
492
493        Ok(())
494    }
495}