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#[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 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 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 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 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 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 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 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 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 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 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}