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#[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 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 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 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(¶ms.form_pairs())
69 .send_bpi_payload::<ShortLinkData>("misc.b23tv.short_link")
70 .await?;
71 data.extract();
72 Ok(data)
73 }
74
75 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(¶ms)
87 .send_bpi_payload("misc.bili_ticket")
88 .await
89 }
90
91 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}