Skip to main content

bpi_rs/comment/
client.rs

1use crate::comment::list::{
2    CommentCountParams, CommentHotParams, CommentListData, CommentListParams, CommentRepliesParams,
3    CountData, HotCommentData,
4};
5use crate::{BilibiliRequest, BpiClient, BpiResult};
6
7const LIST_ENDPOINT: &str = "https://api.bilibili.com/x/v2/reply";
8const REPLIES_ENDPOINT: &str = "https://api.bilibili.com/x/v2/reply/reply";
9const HOT_ENDPOINT: &str = "https://api.bilibili.com/x/v2/reply/hot";
10const COUNT_ENDPOINT: &str = "https://api.bilibili.com/x/v2/reply/count";
11
12/// Comment API client.
13#[derive(Clone, Copy)]
14pub struct CommentClient<'a> {
15    pub(crate) client: &'a BpiClient,
16}
17
18impl<'a> CommentClient<'a> {
19    pub(crate) fn new(client: &'a BpiClient) -> Self {
20        Self { client }
21    }
22
23    #[cfg(test)]
24    pub(crate) fn list_endpoint(&self) -> &'static str {
25        LIST_ENDPOINT
26    }
27
28    #[cfg(test)]
29    pub(crate) fn replies_endpoint(&self) -> &'static str {
30        REPLIES_ENDPOINT
31    }
32
33    #[cfg(test)]
34    pub(crate) fn hot_endpoint(&self) -> &'static str {
35        HOT_ENDPOINT
36    }
37
38    #[cfg(test)]
39    pub(crate) fn count_endpoint(&self) -> &'static str {
40        COUNT_ENDPOINT
41    }
42
43    /// Gets the main comment list for a target comment area.
44    pub async fn list(&self, params: CommentListParams) -> BpiResult<CommentListData> {
45        self.client
46            .get(LIST_ENDPOINT)
47            .query(&params.query_pairs())
48            .send_bpi_payload("comment.read.list")
49            .await
50    }
51
52    /// Gets replies under a root comment.
53    pub async fn replies(&self, params: CommentRepliesParams) -> BpiResult<CommentListData> {
54        self.client
55            .get(REPLIES_ENDPOINT)
56            .query(&params.query_pairs())
57            .send_bpi_payload("comment.read.replies")
58            .await
59    }
60
61    /// Gets hot comments under a root comment when the API returns a payload.
62    pub async fn hot(&self, params: CommentHotParams) -> BpiResult<Option<HotCommentData>> {
63        self.client
64            .get(HOT_ENDPOINT)
65            .query(&params.query_pairs())
66            .send_bpi_optional_payload("comment.read.hot")
67            .await
68    }
69
70    /// Gets the total comment count for a target comment area.
71    pub async fn count(&self, params: CommentCountParams) -> BpiResult<CountData> {
72        self.client
73            .get(COUNT_ENDPOINT)
74            .query(&params.query_pairs())
75            .send_bpi_payload("comment.read.count")
76            .await
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use std::future::Future;
83
84    use crate::comment::list::{
85        CommentCountParams, CommentHotParams, CommentListData, CommentListParams,
86        CommentRepliesParams, CommentSort, CommentTarget, CountData, HotCommentData,
87    };
88    use crate::probe::contract::HttpMethod;
89    use crate::probe::endpoint_contract::EndpointContract;
90    use crate::{BpiClient, BpiResult};
91
92    const TEST_TYPE: i32 = 1;
93    const TEST_OID: i64 = 23199;
94    const TEST_ROOT_RPID: i64 = 2554491176;
95
96    fn target() -> BpiResult<CommentTarget> {
97        CommentTarget::new(TEST_TYPE, TEST_OID)
98    }
99
100    fn list_params() -> BpiResult<CommentListParams> {
101        Ok(CommentListParams::new(target()?)
102            .with_page(1)?
103            .with_page_size(5)?
104            .with_sort(CommentSort::Time)
105            .without_hot(false))
106    }
107
108    fn replies_params() -> BpiResult<CommentRepliesParams> {
109        CommentRepliesParams::new(target()?, TEST_ROOT_RPID)?
110            .with_page(1)?
111            .with_page_size(5)
112    }
113
114    fn hot_params() -> BpiResult<CommentHotParams> {
115        CommentHotParams::new(target()?, TEST_ROOT_RPID)?
116            .with_page(1)?
117            .with_page_size(5)
118    }
119
120    fn assert_list_future<F>(_future: F)
121    where
122        F: Future<Output = BpiResult<CommentListData>>,
123    {
124    }
125
126    fn assert_hot_future<F>(_future: F)
127    where
128        F: Future<Output = BpiResult<Option<HotCommentData>>>,
129    {
130    }
131
132    fn assert_count_future<F>(_future: F)
133    where
134        F: Future<Output = BpiResult<CountData>>,
135    {
136    }
137
138    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
139        let bytes = match endpoint {
140            "list" => {
141                include_bytes!("../../tests/contracts/comment/read/list/contract.json").as_slice()
142            }
143            "replies" => include_bytes!("../../tests/contracts/comment/read/replies/contract.json")
144                .as_slice(),
145            "hot" => {
146                include_bytes!("../../tests/contracts/comment/read/hot/contract.json").as_slice()
147            }
148            "count" => {
149                include_bytes!("../../tests/contracts/comment/read/count/contract.json").as_slice()
150            }
151            _ => unreachable!("unknown comment read contract"),
152        };
153        EndpointContract::from_slice(bytes)
154    }
155
156    #[test]
157    fn comment_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
158        let client = BpiClient::new()?;
159        let comment = client.comment();
160
161        assert_eq!(
162            comment.list_endpoint(),
163            "https://api.bilibili.com/x/v2/reply"
164        );
165        assert_eq!(
166            comment.replies_endpoint(),
167            "https://api.bilibili.com/x/v2/reply/reply"
168        );
169        assert_eq!(
170            comment.hot_endpoint(),
171            "https://api.bilibili.com/x/v2/reply/hot"
172        );
173        assert_eq!(
174            comment.count_endpoint(),
175            "https://api.bilibili.com/x/v2/reply/count"
176        );
177        Ok(())
178    }
179
180    #[test]
181    fn comment_methods_return_payload_futures() -> BpiResult<()> {
182        let client = BpiClient::new()?;
183        let comment = client.comment();
184
185        assert_list_future(comment.list(list_params()?));
186        assert_list_future(comment.replies(replies_params()?));
187        assert_hot_future(comment.hot(hot_params()?));
188        assert_count_future(comment.count(CommentCountParams::new(target()?)));
189        Ok(())
190    }
191
192    #[test]
193    fn comment_contracts_match_module_client_endpoints() -> BpiResult<()> {
194        let client = BpiClient::new()?;
195        let comment = client.comment();
196        let list = contract("list")?;
197        let replies = contract("replies")?;
198        let hot = contract("hot")?;
199        let count = contract("count")?;
200
201        assert_eq!(list.name, "comment.read.list");
202        assert_eq!(list.request.method, HttpMethod::Get);
203        assert_eq!(list.request.url.as_str(), comment.list_endpoint());
204        assert_eq!(
205            list.request.query.get("sort").map(String::as_str),
206            Some("0")
207        );
208
209        assert_eq!(replies.name, "comment.read.replies");
210        assert_eq!(replies.request.method, HttpMethod::Get);
211        assert_eq!(replies.request.url.as_str(), comment.replies_endpoint());
212        assert_eq!(
213            replies.request.query.get("root").map(String::as_str),
214            Some("2554491176")
215        );
216
217        assert_eq!(hot.name, "comment.read.hot");
218        assert_eq!(hot.request.method, HttpMethod::Get);
219        assert_eq!(hot.request.url.as_str(), comment.hot_endpoint());
220        assert!(
221            hot.cases
222                .iter()
223                .all(|case| case.response.rust_model.is_none())
224        );
225
226        assert_eq!(count.name, "comment.read.count");
227        assert_eq!(count.request.method, HttpMethod::Get);
228        assert_eq!(count.request.url.as_str(), comment.count_endpoint());
229        assert_eq!(
230            count.request.query.get("oid").map(String::as_str),
231            Some("23199")
232        );
233        Ok(())
234    }
235}