Skip to main content

bpi_rs/misc/
client.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use crate::misc::MiscB23ShortLinkParams;
4use crate::misc::b23tv::ShortLinkData;
5use crate::misc::buvid::{Buvid3Data, BuvidData};
6use crate::misc::sign::bili_ticket::TicketData;
7use crate::sign::bili_ticket::ticket_request_params;
8use crate::{BilibiliRequest, BpiClient, BpiError, BpiResult};
9
10const BUVID3_ENDPOINT: &str = "https://api.bilibili.com/x/web-frontend/getbuvid";
11const BUVID_ENDPOINT: &str = "https://api.bilibili.com/x/frontend/finger/spi";
12const B23_SHORT_LINK_ENDPOINT: &str = "https://api.biliapi.net/x/share/click";
13const BILI_TICKET_ENDPOINT: &str =
14    "https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket";
15
16/// Misc API client.
17#[derive(Clone, Copy)]
18pub struct MiscClient<'a> {
19    pub(crate) client: &'a BpiClient,
20}
21
22impl<'a> MiscClient<'a> {
23    pub(crate) fn new(client: &'a BpiClient) -> Self {
24        Self { client }
25    }
26
27    #[cfg(test)]
28    pub(crate) fn buvid3_endpoint(&self) -> &'static str {
29        BUVID3_ENDPOINT
30    }
31
32    #[cfg(test)]
33    pub(crate) fn buvid_endpoint(&self) -> &'static str {
34        BUVID_ENDPOINT
35    }
36
37    #[cfg(test)]
38    pub(crate) fn b23_short_link_endpoint(&self) -> &'static str {
39        B23_SHORT_LINK_ENDPOINT
40    }
41
42    #[cfg(test)]
43    pub(crate) fn bili_ticket_endpoint(&self) -> &'static str {
44        BILI_TICKET_ENDPOINT
45    }
46
47    /// Gets a web buvid3 value.
48    pub async fn buvid3(&self) -> BpiResult<Buvid3Data> {
49        self.client
50            .get(BUVID3_ENDPOINT)
51            .send_bpi_payload("misc.buvid3")
52            .await
53    }
54
55    /// Gets web buvid3 and buvid4 values.
56    pub async fn buvid(&self) -> BpiResult<BuvidData> {
57        self.client
58            .get(BUVID_ENDPOINT)
59            .send_bpi_payload("misc.buvid")
60            .await
61    }
62
63    /// Generates a b23.tv short link.
64    pub async fn b23_short_link(&self, params: MiscB23ShortLinkParams) -> BpiResult<ShortLinkData> {
65        let mut data = self
66            .client
67            .post(B23_SHORT_LINK_ENDPOINT)
68            .form(&params.form_pairs())
69            .send_bpi_payload::<ShortLinkData>("misc.b23tv.short_link")
70            .await?;
71        data.extract();
72        Ok(data)
73    }
74
75    /// Generates a bili_ticket payload.
76    pub async fn bili_ticket(&self) -> BpiResult<TicketData> {
77        let csrf = self.client.csrf().unwrap_or_default();
78        let timestamp = SystemTime::now()
79            .duration_since(UNIX_EPOCH)
80            .map_err(|err| BpiError::network(format!("failed to get UNIX timestamp: {err}")))?
81            .as_secs();
82        let params = ticket_request_params(timestamp, csrf.as_str())?;
83
84        self.client
85            .post(BILI_TICKET_ENDPOINT)
86            .query(&params)
87            .send_bpi_payload("misc.bili_ticket")
88            .await
89    }
90
91    /// Generates a bili_ticket and returns only the ticket string.
92    pub async fn bili_ticket_string(&self) -> BpiResult<String> {
93        self.bili_ticket().await.map(|data| data.ticket)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use std::future::Future;
100
101    use crate::ids::Aid;
102    use crate::misc::MiscB23ShortLinkParams;
103    use crate::misc::b23tv::ShortLinkData;
104    use crate::misc::buvid::{Buvid3Data, BuvidData};
105    use crate::misc::sign::bili_ticket::TicketData;
106    use crate::probe::contract::HttpMethod;
107    use crate::probe::endpoint_contract::EndpointContract;
108    use crate::{BpiClient, BpiResult};
109
110    fn assert_buvid3_future<F>(_future: F)
111    where
112        F: Future<Output = BpiResult<Buvid3Data>>,
113    {
114    }
115
116    fn assert_buvid_future<F>(_future: F)
117    where
118        F: Future<Output = BpiResult<BuvidData>>,
119    {
120    }
121
122    fn assert_b23_short_link_future<F>(_future: F)
123    where
124        F: Future<Output = BpiResult<ShortLinkData>>,
125    {
126    }
127
128    fn assert_bili_ticket_future<F>(_future: F)
129    where
130        F: Future<Output = BpiResult<TicketData>>,
131    {
132    }
133
134    fn assert_bili_ticket_string_future<F>(_future: F)
135    where
136        F: Future<Output = BpiResult<String>>,
137    {
138    }
139
140    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
141        let bytes = match endpoint {
142            "buvid3" => {
143                include_bytes!("../../tests/contracts/misc/buvid3/contract.json").as_slice()
144            }
145            "buvid" => include_bytes!("../../tests/contracts/misc/buvid/contract.json").as_slice(),
146            "b23-short-link" => {
147                include_bytes!("../../tests/contracts/misc/b23tv/short-link/contract.json")
148                    .as_slice()
149            }
150            "bili-ticket" => {
151                include_bytes!("../../tests/contracts/misc/sign/bili-ticket/contract.json")
152                    .as_slice()
153            }
154            _ => unreachable!("unknown misc endpoint fixture"),
155        };
156
157        EndpointContract::from_slice(bytes)
158    }
159
160    #[test]
161    fn misc_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
162        let client = BpiClient::new()?;
163        let misc = client.misc();
164
165        assert_eq!(
166            misc.buvid3_endpoint(),
167            "https://api.bilibili.com/x/web-frontend/getbuvid"
168        );
169        assert_eq!(
170            misc.buvid_endpoint(),
171            "https://api.bilibili.com/x/frontend/finger/spi"
172        );
173        assert_eq!(
174            misc.b23_short_link_endpoint(),
175            "https://api.biliapi.net/x/share/click"
176        );
177        assert_eq!(
178            misc.bili_ticket_endpoint(),
179            "https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket"
180        );
181        Ok(())
182    }
183
184    #[test]
185    fn misc_methods_return_payload_futures() -> BpiResult<()> {
186        let client = BpiClient::new()?;
187        let misc = client.misc();
188
189        assert_buvid3_future(misc.buvid3());
190        assert_buvid_future(misc.buvid());
191        assert_b23_short_link_future(
192            misc.b23_short_link(MiscB23ShortLinkParams::new(Aid::new(10001)?)),
193        );
194        assert_bili_ticket_future(misc.bili_ticket());
195        assert_bili_ticket_string_future(misc.bili_ticket_string());
196        Ok(())
197    }
198
199    #[test]
200    fn misc_contracts_match_module_client_endpoints() -> BpiResult<()> {
201        let client = BpiClient::new()?;
202        let misc = client.misc();
203        let buvid3 = contract("buvid3")?;
204        let buvid = contract("buvid")?;
205        let b23_short_link = contract("b23-short-link")?;
206        let bili_ticket = contract("bili-ticket")?;
207
208        assert_eq!(buvid3.name, "misc.buvid3");
209        assert_eq!(buvid3.request.method, HttpMethod::Get);
210        assert_eq!(buvid3.request.url.as_str(), misc.buvid3_endpoint());
211        assert!(buvid3.request.query.is_empty());
212
213        assert_eq!(buvid.name, "misc.buvid");
214        assert_eq!(buvid.request.method, HttpMethod::Get);
215        assert_eq!(buvid.request.url.as_str(), misc.buvid_endpoint());
216        assert!(buvid.request.query.is_empty());
217
218        assert_eq!(b23_short_link.name, "misc.b23tv.short_link");
219        assert_eq!(b23_short_link.request.method, HttpMethod::Post);
220        assert_eq!(
221            b23_short_link.request.url.as_str(),
222            misc.b23_short_link_endpoint()
223        );
224        assert!(b23_short_link.request.query.is_empty());
225
226        assert_eq!(bili_ticket.name, "misc.bili_ticket");
227        assert_eq!(bili_ticket.request.method, HttpMethod::Post);
228        assert_eq!(
229            bili_ticket.request.url.as_str(),
230            misc.bili_ticket_endpoint()
231        );
232        assert_eq!(
233            bili_ticket.request.query.get("csrf").map(String::as_str),
234            Some("${csrf}")
235        );
236        Ok(())
237    }
238}