1use crate::article::articles::ArticlesData;
2use crate::article::card::CardData;
3use crate::article::info::ArticleInfoData;
4use crate::article::params::{
5 ArticleArticlesInfoParams, ArticleCardsParams, ArticleInfoParams, ArticleViewParams,
6};
7use crate::article::view::ArticleViewData;
8use crate::{BilibiliRequest, BpiClient, BpiResult};
9
10const INFO_ENDPOINT: &str = "https://api.bilibili.com/x/article/viewinfo";
11const VIEW_ENDPOINT: &str = "https://api.bilibili.com/x/article/view";
12const CARDS_ENDPOINT: &str = "https://api.bilibili.com/x/article/cards";
13const ARTICLES_ENDPOINT: &str = "https://api.bilibili.com/x/article/list/web/articles";
14
15#[derive(Clone, Copy)]
17pub struct ArticleClient<'a> {
18 pub(crate) client: &'a BpiClient,
19}
20
21impl<'a> ArticleClient<'a> {
22 pub(crate) fn new(client: &'a BpiClient) -> Self {
23 Self { client }
24 }
25
26 #[cfg(test)]
27 pub(crate) fn info_endpoint(&self) -> &'static str {
28 INFO_ENDPOINT
29 }
30
31 #[cfg(test)]
32 pub(crate) fn view_endpoint(&self) -> &'static str {
33 VIEW_ENDPOINT
34 }
35
36 #[cfg(test)]
37 pub(crate) fn cards_endpoint(&self) -> &'static str {
38 CARDS_ENDPOINT
39 }
40
41 #[cfg(test)]
42 pub(crate) fn articles_endpoint(&self) -> &'static str {
43 ARTICLES_ENDPOINT
44 }
45
46 pub async fn info(&self, params: ArticleInfoParams) -> BpiResult<ArticleInfoData> {
48 self.client
49 .get(INFO_ENDPOINT)
50 .query(¶ms.query_pairs())
51 .send_bpi_payload("article.info")
52 .await
53 }
54
55 pub async fn view(&self, params: ArticleViewParams) -> BpiResult<ArticleViewData> {
57 let signed_params = self.client.get_wbi_sign2(params.query_pairs()).await?;
58
59 self.client
60 .get(VIEW_ENDPOINT)
61 .query(&signed_params)
62 .send_bpi_payload("article.view")
63 .await
64 }
65
66 pub async fn cards(&self, params: ArticleCardsParams) -> BpiResult<CardData> {
68 let signed_params = self.client.get_wbi_sign2(params.query_pairs()).await?;
69
70 self.client
71 .get(CARDS_ENDPOINT)
72 .query(&signed_params)
73 .send_bpi_payload("article.cards")
74 .await
75 }
76
77 pub async fn articles(&self, params: ArticleArticlesInfoParams) -> BpiResult<ArticlesData> {
79 self.client
80 .get(ARTICLES_ENDPOINT)
81 .query(¶ms.query_pairs())
82 .send_bpi_payload("article.articles_info")
83 .await
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use std::future::Future;
90
91 use crate::article::articles::ArticlesData;
92 use crate::article::card::CardData;
93 use crate::article::info::ArticleInfoData;
94 use crate::article::params::{
95 ArticleArticlesInfoParams, ArticleCardsParams, ArticleInfoParams, ArticleViewParams,
96 };
97 use crate::article::view::ArticleViewData;
98 use crate::probe::contract::HttpMethod;
99 use crate::probe::endpoint_contract::EndpointContract;
100 use crate::{BpiClient, BpiResult};
101
102 const TEST_CVID: i64 = 2;
103 const TEST_LIST_ID: i64 = 207146;
104
105 fn assert_info_future<F>(_future: F)
106 where
107 F: Future<Output = BpiResult<ArticleInfoData>>,
108 {
109 }
110
111 fn assert_view_future<F>(_future: F)
112 where
113 F: Future<Output = BpiResult<ArticleViewData>>,
114 {
115 }
116
117 fn assert_cards_future<F>(_future: F)
118 where
119 F: Future<Output = BpiResult<CardData>>,
120 {
121 }
122
123 fn assert_articles_future<F>(_future: F)
124 where
125 F: Future<Output = BpiResult<ArticlesData>>,
126 {
127 }
128
129 fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
130 let bytes = match endpoint {
131 "info" => include_bytes!("../../tests/contracts/article/info/contract.json").as_slice(),
132 "view" => include_bytes!("../../tests/contracts/article/view/contract.json").as_slice(),
133 "cards" => {
134 include_bytes!("../../tests/contracts/article/cards/contract.json").as_slice()
135 }
136 "articles" => {
137 include_bytes!("../../tests/contracts/article/articles/contract.json").as_slice()
138 }
139 _ => unreachable!("unknown article contract"),
140 };
141 EndpointContract::from_slice(bytes)
142 }
143
144 #[test]
145 fn article_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
146 let client = BpiClient::new()?;
147 let article = client.article();
148
149 assert_eq!(
150 article.info_endpoint(),
151 "https://api.bilibili.com/x/article/viewinfo"
152 );
153 assert_eq!(
154 article.view_endpoint(),
155 "https://api.bilibili.com/x/article/view"
156 );
157 assert_eq!(
158 article.cards_endpoint(),
159 "https://api.bilibili.com/x/article/cards"
160 );
161 assert_eq!(
162 article.articles_endpoint(),
163 "https://api.bilibili.com/x/article/list/web/articles"
164 );
165 Ok(())
166 }
167
168 #[test]
169 fn article_methods_return_payload_futures() -> BpiResult<()> {
170 let client = BpiClient::new()?;
171 let article = client.article();
172
173 assert_info_future(article.info(ArticleInfoParams::new(TEST_CVID)?));
174 assert_view_future(article.view(ArticleViewParams::new(TEST_CVID)?));
175 assert_cards_future(article.cards(ArticleCardsParams::new("av2,cv1,cv2")?));
176 assert_articles_future(article.articles(ArticleArticlesInfoParams::new(TEST_LIST_ID)?));
177 Ok(())
178 }
179
180 #[test]
181 fn article_contracts_match_module_client_endpoints() -> BpiResult<()> {
182 let client = BpiClient::new()?;
183 let article = client.article();
184 let info = contract("info")?;
185 let view = contract("view")?;
186 let cards = contract("cards")?;
187 let articles = contract("articles")?;
188
189 assert_eq!(info.name, "article.info");
190 assert_eq!(info.request.method, HttpMethod::Get);
191 assert_eq!(info.request.url.as_str(), article.info_endpoint());
192 assert_eq!(info.request.query.get("id").map(String::as_str), Some("2"));
193
194 assert_eq!(view.name, "article.view");
195 assert_eq!(view.request.method, HttpMethod::Get);
196 assert_eq!(view.request.url.as_str(), article.view_endpoint());
197 assert!(view.request.auth.requires_wbi());
198
199 assert_eq!(cards.name, "article.cards");
200 assert_eq!(cards.request.method, HttpMethod::Get);
201 assert_eq!(cards.request.url.as_str(), article.cards_endpoint());
202 assert!(cards.request.auth.requires_wbi());
203
204 assert_eq!(articles.name, "article.articles_info");
205 assert_eq!(articles.request.method, HttpMethod::Get);
206 assert_eq!(articles.request.url.as_str(), article.articles_endpoint());
207 assert_eq!(
208 articles.request.query.get("id").map(String::as_str),
209 Some("207146")
210 );
211 Ok(())
212 }
213}