Skip to main content

bpi_rs/article/
client.rs

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/// Article API client.
16#[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    /// Gets article summary information.
47    pub async fn info(&self, params: ArticleInfoParams) -> BpiResult<ArticleInfoData> {
48        self.client
49            .get(INFO_ENDPOINT)
50            .query(&params.query_pairs())
51            .send_bpi_payload("article.info")
52            .await
53    }
54
55    /// Gets article content.
56    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    /// Gets article, video, or live cards referenced by article content.
67    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    /// Gets article list information.
78    pub async fn articles(&self, params: ArticleArticlesInfoParams) -> BpiResult<ArticlesData> {
79        self.client
80            .get(ARTICLES_ENDPOINT)
81            .query(&params.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}